Introduzione¶

L'obiettivo di questo progetto è quello di applicare tecniche di analisi dati adottando un approccio data-driven per il lancio di una ipotetica nuova applicazione mobile.

Per realizzare questa analisi, vi sono a disposizione due fonti di dati. La prima è un dataset completo delle applicazioni presenti sul Play Store che fornisce informazioni dettagliate su ogni app: dai rating alle recensioni, dal numero di installazioni al prezzo, fino alle caratteristiche tecniche. La seconda fonte è una raccolta di recensioni degli utenti, già elaborate con tecniche di sentiment analysis.

Partendo dalle fondamenta, ho cercato di creare una struttura robusta per la gestione dei dati, con particolare attenzione alla pulizia e alla preparazione degli stessi.

Un aspetto particolarmente interessante dell'analisi sarà lo studio della competizione nelle diverse categorie. Alcune categorie potrebbero sembrare attraenti a prima vista, magari per l'alto numero di download, ma potrebbero rivelarsi mercati saturi con forte competizione. Altre categorie, apparentemente più piccole, potrebbero nascondere nicchie interessanti con meno concorrenza e, potenzialmente, utenti più disposti a pagare per un prodotto di qualità.

Il codice è stato strutturato seguendo un approccio modulare, con particolare attenzione alla chiarezza e alla documentazione. Ogni fase dell'analisi è organizzata in componenti logiche ben definite, permettendo di seguire facilmente il processo di analisi dai dati grezzi fino alle conclusioni finali. Questa struttura non solo dovrebbe rendere il codice più robusto, ma facilita anche la verifica e la validazione dei risultati ottenuti in ogni fase dell'analisi.

1. Import e setup¶

La seguente cella di codice ha lo scopo di preparare l'ambiente di lavoro per l'analisi dei dati. Si occupa quindi dell'importazione delle librerie e della configurazione dell'ambiente di analisi.

Le librerie utilizzate sono le seguenti:

  • warning: per gestire e disabilitare i messaggi di avvertimento che potrebbero disturbare l'output dell'analisi;
  • logging: per tracciare le varie fasi dell'analisi e catturare eventuali errori o comportamenti inaspettati durante l'elaborazione dei dati;
  • typing: permette di specificare i tipi di dati attesi per ogni variabile e funzione, rendendo il codice più robusto e auto-documentato. Nel mio caso l'ho utilizzato per definire chiaramente cosa aspettarsi da input e output delle funzioni, ad esempio con Dict per i dizionari o Optional per valori che potrebbero essere None;
  • dataclasses: semplifica la creazione di classi destinate a contenere dati;
  • pathlib e os: librerie che lavorano insieme per gestire le operazioni sui file, come la verifica dell'esistenza dei CSV e la gestione dei percorsi, indipendentemente dal sistema operativo utilizzato;
  • lru_cache da functools: implementa una memoria cache per le funzioni di formattazione più utilizzate, evitando di ricalcolare risultati già ottenuti e migliorando le performance;
  • ThreadPoolExecutor da concurrent.futures: permette di parallelizzare alcune operazioni di elaborazione dati particolarmente pesanti, migliorando le performance dell'analisi;
  • re: per le operazioni di pulizia e standardizzazione dei dati, in particolare per estrarre informazioni numeriche da stringhe come prezzi e dimensioni delle app;
  • datetime: necessario per l'analisi temporale dei dati, in particolare per calcolare intervalli di tempo;
  • pandas: per creare e manipolare dataframe;
  • numpy: complementare a pandas, fornisce le funzionalità per calcoli numerici avanzati, come il calcolo di metriche, statistiche e la gestione di array multidimensionali necessari per l'analisi delle performance delle app;
  • pandas.api.types: per implementare controlli più rigidi sui tipi di dati nelle colonne dei dataframe, assicurando che le operazioni numeriche vengano eseguite solo su dati appropriati.

 

Per la visualizzazione dei dati, ho scelto di utilizzare due approcci complementari:

  • plotly (con i suoi moduli express, graph_objects e subplots) per creare visualizzazioni interattive e dettagliate delle metriche del Play Store, permettendo un'esplorazione dinamica dei dati;
  • matplotlib.pyplot e seaborn per generare visualizzazioni statistiche più tradizionali, particolarmente utili per l'analisi delle distribuzioni e delle correlazioni.

I warning vengono disabilitati con warnings.filterwarnings('ignore') per mantenere l'output pulito, mentre il sistema di logging viene configurato attraverso logging.basicConfig() per tracciare operazioni ed errori.

 

La classe PlotConfig, definita usando il decoratore @dataclass, centralizza le configurazioni per la visualizzazione.

COLOR_PALETTE mappa stati come 'primary', 'success', 'warning' ai rispettivi codici colore esadecimali, mentre PLOT_STYLE definisce uno stile uniforme per i grafici con font, dimensioni e caratteristiche di base.

l metodo __post_init__ viene chiamato automaticamente dopo il metodo __init__ e viene usato per definire i valori di default di COLOR_PALETTE e PLOT_STYLE.

Ho poi implementato la classe DataFormatter per gestire la formattazione dei dati. Contiene tre metodi statici, ciascuno decorato con @lru_cache(maxsize=1000) per la memorizzazione dei risultati: format_number(), format_percentage() e format_currency(), che gestiscono rispettivamente numeri con separatori di migliaia, percentuali con un decimale e valori monetari.

Il decoratore @staticmethod indica appunto che questi sono metodi statici, il che significa che possono essere chiamati direttamente sulla classe senza creare un'istanza della stessa.

 

La classe DataLoader costituisce il nucleo del caricamento dati. Il metodo _is_colab_environment() verifica l'esecuzione su Google Colab, mentre _setup_visualization_settings() configura le impostazioni di pandas e degli strumenti di visualizzazione. Il metodo principale load_data() gestisce il caricamento dei file CSV attraverso un blocco try-except, registrando eventuali errori tramite il logger. L'inizializzazione avviene con data_loader = DataLoader(), seguito dal tentativo di caricamento dei dati. Un controllo con if apps_df is None or reviews_df is None verifica il successo dell'operazione, terminando l'esecuzione con sys.exit(1) in caso di errore.

 

Per quanto riguarda le librerie di visualizzazione, plotly.express e plotly.graph_objects permetteranno di creare grafici interattivi, mentre matplotlib.pyplot e seaborn saranno utilizzati per visualizzazioni statistiche tradizionali.

 

La manipolazione e l'analisi dei dati numerici si baseranno su numpy e pandas.

In [ ]:
# Gestione warning e logging
import warnings
import logging
from typing import Dict, List, Tuple, Optional, Any, NamedTuple, Union
from dataclasses import dataclass, field
from pathlib import Path
import sys
import os
from functools import lru_cache
from concurrent.futures import ThreadPoolExecutor
import re
from datetime import datetime

# Disabilita warnings
warnings.filterwarnings('ignore')

# Setup logging base
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

# Librerie di base per l'analisi dati
import pandas as pd
import numpy as np
from pandas.api.types import is_numeric_dtype

# Librerie per visualizzazione
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import matplotlib.pyplot as plt
import seaborn as sns

@dataclass
class PlotConfig:
    COLOR_PALETTE: Dict[str, str] = None
    PLOT_STYLE: Dict[str, Any] = None

    def __post_init__(self):
        self.COLOR_PALETTE = {
            'primary': '#2c3e50',
            'secondary': '#34495e',
            'success': '#27ae60',
            'warning': '#f39c12',
            'danger': '#c0392b',
            'info': '#3498db'
        }

        self.PLOT_STYLE = {
            'template': 'plotly_white',
            'font_family': 'Arial, sans-serif',
            'title_font_size': 20,
            'title_x': 0.5,
            'showlegend': True
        }

class DataFormatter:

    @staticmethod
    @lru_cache(maxsize=1000)
    def format_number(num: Union[int, float]) -> str:
        """Formatta numeri con separatori di migliaia"""
        return f"{num:,.0f}"

    @staticmethod
    @lru_cache(maxsize=1000)
    def format_percentage(num: Union[int, float]) -> str:
        """Formatta percentuali con 1 decimale"""
        return f"{num:.1f}%"

    @staticmethod
    @lru_cache(maxsize=1000)
    def format_currency(num: Union[int, float]) -> str:
        """Formatta valori monetari"""
        return f"${num:,.2f}"

class DataLoader:

    def __init__(self):
        self.plot_config = PlotConfig()

    def _is_colab_environment(self) -> bool:
        try:
            import google.colab
            return True
        except ImportError:
            return False

    def _setup_visualization_settings(self):

        # Impostazioni pandas
        pd.set_option('display.max_columns', None)
        pd.set_option('display.max_rows', 100)
        pd.set_option('display.float_format', lambda x: '%.3f' % x)

        # Impostazioni matplotlib/seaborn
        plt.style.use('default')
        sns.set_theme(style='whitegrid')

    def load_data(self) -> Tuple[Optional[pd.DataFrame], Optional[pd.DataFrame]]:
        try:
            # Setup visualizzazione
            self._setup_visualization_settings()

            # Determina l' ambiente e carica i dati
            if self._is_colab_environment():
                logger.info("Ambiente rilevato: Google Colab")
                from google.colab import files
                logger.info("Per favore, carica i file 'googleplaystore.csv' e 'googleplaystore_user_reviews.csv'")
                uploaded = files.upload()

            # Verifica presenza file
            required_files = ['googleplaystore.csv', 'googleplaystore_user_reviews.csv']
            for file in required_files:
                if not os.path.exists(file):
                    raise FileNotFoundError(f"File {file} non trovato nella directory corrente")

            apps_df = pd.read_csv('googleplaystore.csv')
            reviews_df = pd.read_csv('googleplaystore_user_reviews.csv')

            logger.info(f"Dataset caricati con successo!")
            logger.info(f"Dimensioni apps_df: {apps_df.shape}")
            logger.info(f"Dimensioni reviews_df: {reviews_df.shape}")

            return apps_df, reviews_df

        except Exception as e:
            logger.error(f"Errore nel caricamento dei dati: {str(e)}")
            return None, None

# Inizializzazione del loader e caricamento dei dati
data_loader = DataLoader()
apps_df, reviews_df = data_loader.load_data()

# Verifica che i dati siano stati caricati correttamente
if apps_df is None or reviews_df is None:
    logger.error("Errore nel caricamento dei dataset. Verifica la presenza dei file o ricaricali.")
    sys.exit(1)
Upload widget is only available when the cell has been executed in the current browser session. Please rerun this cell to enable.
Saving googleplaystore_user_reviews.csv to googleplaystore_user_reviews.csv
Saving googleplaystore.csv to googleplaystore.csv

2. Lettura e validazione dei dati¶

Il secondo blocco del codice si occupa della validazione e dell'analisi iniziale dei due dataset principali: apps_df, che contiene le informazioni sulle app del Google Play Store, e reviews_df, che contiene le recensioni degli utenti.

All'inizio della cella importo partial da functools, che utilizzerò per la parallelizzazione delle operazioni di validazione. Vado a definire poi una serie di classi specializzate per gestire diversi aspetti della validazione dei dati.

 

La classe DatasetMetrics è definita come @dataclass e funge da contenitore per le metriche principali di un dataset. Include campi come:

  • rows e columns per le dimensioni;
  • missing_data per tracciare le percentuali di valori mancanti per colonna;
  • duplicates e duplicate_percentage per i record duplicati;
  • dtypes per i tipi di dati delle colonne;
  • unique_counts per contare i valori unici in ogni colonna.

 

La classe DataValidator implementa la logica di validazione vera e propria. Nel costruttore accetta max_workers per controllare la parallelizzazione.

Il metodo _calculate_column_metrics è anch'esso decorato con @staticmethod perché non necessita di accedere allo stato dell'istanza della classe. Prende in input un dataframe (df) e il nome di una colonna (column) e restituisce un dizionario con le seguenti metriche:

  • tipo: il tipo di dati della colonna ottenuto con dtype e convertito in stringa;
  • non_nulli: il numero di valori non nulli calcolato con count();
  • nulli: il numero di valori nulli ottenuto con isnull().sum()
  • perc_nulli: la percentuale di valori nulli calcolata come (nulli/totale)*100 e arrotondata a 2 decimali
  • unique_values: il numero di valori unici nella colonna ottenuto con nunique().

Il metodo validate_dataset è il cuore della validazione e utilizza ThreadPoolExecutor per parallelizzare i calcoli sulle colonne. Attraverso executor.map e partial, applica _calculate_column_metrics a tutte le colonne contemporaneamente. Calcola anche il numero di duplicati nel dataset usando duplicated().sum().

 

Ho poi implementato la classe DataConsistencyChecker per verificare la coerenza tra i due dataset. Il suo metodo check_consistency (decorato con @staticmethod) usa operazioni su insiemi per confrontare le app presenti nei dataset:

  • crea due set con unique() per ottenere le app uniche in ciascun dataset;
  • usa intersection per trovare le app presenti in entrambi;
  • usa l'operatore - per identificare le app con recensioni ma assenti nel dataset principale.

Durante l'esecuzione, vengono registrate nel log diverse statistiche utili sulla distribuzione delle app tra i dataset. Il metodo tiene traccia del numero totale di app in ciascun dataset, di quante sono in comune e genera un warning se trova app che hanno recensioni ma non esistono nel dataset principale. Viene anche verificata l'integrità complessiva dei dati, registrando il numero di app uniche nel Play Store e il totale dei record nelle recensioni. Al termine della verifica, il metodo restituisce i dataframe originali in una tupla, senza apportare modifiche.

 

La classe InitialAnalyzer fornisce una prima panoramica dei dati. Il suo metodo analyze_dataset separa le colonne in numeriche e categoriche usando select_dtypes:

  • per le colonne numeriche usa describe() per ottenere statistiche di base;
  • per le colonne categoriche conta i valori unici e, se sono meno di 10, calcola la distribuzione con value_counts(normalize=True).

La funzione principale validate_and_analyze_data di fatto gestisce tutto il processo:

  1. inizializza le classi necessarie se non fornite;
  2. esegue la validazione di entrambi i dataset;
  3. verifica la loro consistenza;
  4. conduce l'analisi iniziale.

Il tutto è gestito in un blocco try-except per catturare e loggare eventuali errori.

 

L'utilizzo di logger attraverso tutto il codice permette di tenere traccia dettagliata del processo e dei suoi risultati, facilitando l'identificazione di eventuali problemi nei dati.

In [ ]:
from functools import partial


logger = logging.getLogger(__name__)

@dataclass
class DatasetMetrics:
    rows: int
    columns: int
    missing_data: Dict[str, float]
    duplicates: int
    duplicate_percentage: float
    dtypes: Dict[str, str]
    unique_counts: Dict[str, int]

class DataValidator:

    def __init__(self, max_workers: int = 4):
        self.max_workers = max_workers

    @staticmethod
    def _calculate_column_metrics(df: pd.DataFrame, column: str) -> Dict[str, Any]:
        return {
            'tipo': str(df[column].dtype),
            'non_nulli': df[column].count(),
            'nulli': df[column].isnull().sum(),
            'perc_nulli': (df[column].isnull().sum() / len(df) * 100).round(2),
            'unique_values': df[column].nunique()
        }

    def validate_dataset(self, df: pd.DataFrame, dataset_name: str) -> DatasetMetrics:
        logger.info(f"\nValidazione dataset: {dataset_name}")
        logger.info("-" * 50)

        # Calcolo metriche base
        rows, cols = df.shape
        logger.info(f"Dimensioni: {rows:,} righe, {cols} colonne")

        # Calcolo metriche per colonna in parallelo
        with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
            column_metrics = dict(zip(
                df.columns,
                executor.map(partial(self._calculate_column_metrics, df), df.columns)
            ))

        # Calcolo duplicati
        duplicates = df.duplicated().sum()
        duplicate_percentage = (duplicates/len(df)*100).round(2)

        logger.info(f"\nDuplicati trovati: {duplicates:,} ({duplicate_percentage}%)")

        return DatasetMetrics(
            rows=rows,
            columns=cols,
            missing_data={col: metrics['perc_nulli']
                         for col, metrics in column_metrics.items()},
            duplicates=duplicates,
            duplicate_percentage=duplicate_percentage,
            dtypes={col: metrics['tipo']
                   for col, metrics in column_metrics.items()},
            unique_counts={col: metrics['unique_values']
                         for col, metrics in column_metrics.items()}
        )

class DataConsistencyChecker:

    @staticmethod
    def check_consistency(apps_df: pd.DataFrame,
                         reviews_df: pd.DataFrame) -> Tuple[pd.DataFrame, pd.DataFrame]:
        logger.info("\nVerifica consistenza tra dataset")
        logger.info("-" * 50)

        # Verifica app presenti in entrambi i dataset
        apps_in_store = set(apps_df['App'].unique())
        apps_in_reviews = set(reviews_df['App'].unique())

        common_apps = apps_in_store.intersection(apps_in_reviews)
        missing_apps = apps_in_reviews - apps_in_store

        logger.info(f"App nel Play Store: {len(apps_in_store):,}")
        logger.info(f"App con recensioni: {len(apps_in_reviews):,}")
        logger.info(f"App in comune: {len(common_apps):,}")

        if missing_apps:
            logger.warning(
                f"\nAttenzione: {len(missing_apps):,} app hanno recensioni "
                "ma non sono nel dataset principale"
            )

        # Verifica integrità
        logger.info("\nVerifica integrità dei dati:")
        logger.info(f"- App univoche nel Play Store: {apps_df['App'].nunique():,}")
        logger.info(f"- Record totali recensioni: {len(reviews_df):,}")

        return apps_df, reviews_df

class InitialAnalyzer:

    @staticmethod
    def analyze_dataset(df: pd.DataFrame, dataset_name: str) -> None:
        logger.info(f"\nAnalisi iniziale: {dataset_name}")
        logger.info("-" * 50)

        # Analisi colonne numeriche
        numeric_cols = df.select_dtypes(include=[np.number]).columns
        if len(numeric_cols) > 0:
            logger.info("\nStatistiche colonne numeriche:")
            logger.info(df[numeric_cols].describe().round(2))

        # Analisi colonne categoriche
        categorical_cols = df.select_dtypes(include=['object']).columns
        if len(categorical_cols) > 0:
            logger.info("\nStatistiche colonne categoriche:")
            for col in categorical_cols:
                unique_vals = df[col].nunique()
                logger.info(f"\n{col}:")
                logger.info(f"- Valori unici: {unique_vals:,}")
                if unique_vals < 10:
                    dist = df[col].value_counts(normalize=True).head()
                    logger.info(f"Distribuzione:\n{dist.round(3)}")

def validate_and_analyze_data(apps_df: pd.DataFrame,
                            reviews_df: pd.DataFrame,
                            validator: Optional[DataValidator] = None,
                            consistency_checker: Optional[DataConsistencyChecker] = None,
                            initial_analyzer: Optional[InitialAnalyzer] = None) -> Tuple[pd.DataFrame, pd.DataFrame]:
    logger.info("=== INIZIO VALIDAZIONE E ANALISI DATI ===")

    # Inizializzazione componenti se non forniti
    validator = validator or DataValidator()
    consistency_checker = consistency_checker or DataConsistencyChecker()
    initial_analyzer = initial_analyzer or InitialAnalyzer()

    try:
        # Validazione dataset
        apps_metrics = validator.validate_dataset(apps_df, "Google Play Store Apps")
        reviews_metrics = validator.validate_dataset(reviews_df, "App Reviews")

        # Verifica della consistenza
        apps_df, reviews_df = consistency_checker.check_consistency(apps_df, reviews_df)

        # Analisi iniziale
        initial_analyzer.analyze_dataset(apps_df, "Google Play Store Apps")
        initial_analyzer.analyze_dataset(reviews_df, "App Reviews")

        return apps_df, reviews_df

    except Exception as e:
        logger.error(f"Errore durante la validazione e analisi: {str(e)}")
        raise

# Esecuzione della validazione e analisi
apps_df, reviews_df = validate_and_analyze_data(apps_df, reviews_df)
WARNING:__main__:
Attenzione: 54 app hanno recensioni ma non sono nel dataset principale

3. Pulizia e preparazione dei dati¶

La terza cella di codice si concentra sulla pulizia e preparazione dei dati, una fase estremamente importante per garantire che la successiva analisi sia basata su informazioni accurate e ben strutturate.

  • clean_size() converte le dimensioni delle app in megabyte. Gestisce casi come 'Varies with device' e converte automaticamente i KB in MB dividendo per 1024 quando necessario. Utilizza espressioni regolari con re.sub(r'[^0-9.]', '') per estrarre solo i numeri dalla stringa;
  • clean_price() standardizza i valori di prezzo in formato numerico, convertendo stringhe come '$4.99' in valori decimali (4.99) e gestendo casi speciali come 'Free' che vengono trasformati in 0.0;
  • clean_installs() converte il numero di installazioni in interi, rimuovendo caratteri come virgole e il segno '+' (es. '1,000,000+' diventa 1000000);
  • clean_android_version() estrae e normalizza la versione Android, utilizzando un'espressione regolare per trovare il formato numerico principale (es. da "Android 4.0.3 or up" estrae 4.0).

 

Da questa classe base derivano due classi specializzate. La prima è AppsDataCleaner, dedicata alla pulizia del dataset delle applicazioni. Questa classe implementa _parallel_clean_row(), che pulisce una singola riga applicando i metodi di pulizia e aggiungendo nuove colonne pulite e clean_apps_dataset(), che coordina l'intero processo utilizzando ThreadPoolExecutor per parallelizzare la pulizia e migliorare le performance.

Durante il processo di pulizia delle app vengono eseguite diverse operazioni avanzate:

  • conversione dei tipi di dati usando pd.to_numeric() e pd.to_datetime();
  • rimozione delle righe con valori mancanti attraverso dropna();
  • feature engineering con la creazione della variabile Days_Since_Update, che indica il numero di giorni trascorsi dall'ultimo aggiornamento di ogni record, allo scopo di fornire informazioni temporali utili per l'analisi dei dati;
  • categorizzazioni di variabili continue come prezzo e installazioni usando pd.cut();
  • calcolo di metriche di mercato come Market_Share e Category_Share.

 

La seconda classe derivata, ReviewsDataCleaner, è specifica per il dataset delle recensioni. Sebbene più semplice, si occupa di svolgere diverse attività, ovvero:

  • pulire i valori mancanti nelle colonne del sentiment;
  • convertire dei tipi di dati per le metriche di polarità e soggettività;
  • creare la feature Review_Length per analizzare la lunghezza delle recensioni;
  • categorizzare la polarità del sentiment in "Negative", "Neutral" e "Positive".

Ci tengo a specificare che queste funzionalità non vengono poi sfruttate appieno nelle analisi successive. Inizialmente avevo pianificato di sviluppare visualizzazioni e insights basati sul sentiment e sulle recensioni degli utenti, ma durante l'analisi ho riscontrato che questi dati non producevano risultati sufficientemente significativi o interpretabili per gli obiettivi del progetto. Ho deciso quindi di focalizzare l'attenzione sull'analisi delle metriche delle app (rating, installazioni, prezzo) che hanno fornito insights più concreti per identificare le opportunità di mercato. Nonostante le recensioni non vengano utilizzate nelle analisi successive, ho mantenuto questa parte del codice di pulizia per completezza metodologica e per eventuali approfondimenti futuri.

Infine, la funzione principale clean_datasets() gestisce l'intero processo di pulizia. Inizializza i cleaner necessari, esegue la pulizia di entrambi i dataset e registra statistiche dettagliate sulle trasformazioni effettuate. Il tutto è incapsulato in un blocco try-except per gestire eventuali errori durante il processo.

 

Il risultato finale sono due DataFrame puliti e arricchiti, apps_clean e reviews_clean, pronti per le analisi esplorative successive.

In [ ]:
logger = logging.getLogger(__name__)

@dataclass
class CleaningReport:
    original_rows: int
    cleaned_rows: int
    removed_rows: int
    missing_before: Dict[str, int]
    missing_after: Dict[str, int]
    cleaning_steps: list[str]

class DataCleaner:

    @staticmethod
    def clean_size(size: str) -> Optional[float]:
        """Converte la dimensione dell'app in MB"""
        if pd.isna(size) or size == 'Varies with device':
            return np.nan
        try:
            size_str = str(size).strip().upper()
            multiplier = 1/1024 if 'K' in size_str else 1
            return float(re.sub(r'[^0-9.]', '', size_str)) * multiplier
        except (ValueError, AttributeError):
            return np.nan

    @staticmethod
    def clean_price(price: str) -> float:
        """Converte il prezzo in valore numerico"""
        if pd.isna(price) or price in ['Free', '0', 'Everyone']:
            return 0.0
        try:
            return float(re.sub(r'[^0-9.]', '', str(price)))
        except (ValueError, AttributeError):
            return 0.0

    @staticmethod
    def clean_installs(installs: str) -> int:
        """Converte il numero di installazioni in valore numerico"""
        if pd.isna(installs):
            return 0
        try:
            return int(str(installs).replace(',', '').replace('+', '').strip())
        except ValueError:
            return 0

    @staticmethod
    def clean_android_version(version: str) -> Optional[float]:
        """Estrae e normalizza la versione Android"""
        if pd.isna(version):
            return np.nan
        try:
            match = re.search(r'(\d+\.?\d?)', str(version))
            return round(float(match.group(1)), 1) if match else np.nan
        except (ValueError, AttributeError):
            return np.nan

class AppsDataCleaner(DataCleaner):

    def __init__(self, max_workers: int = 4):
        self.max_workers = max_workers
        self.cleaning_steps = []

    def _parallel_clean_row(self, row: pd.Series) -> pd.Series:
        """Pulisce una singola riga del dataset in parallelo"""
        row = row.copy()
        row['Size_MB'] = self.clean_size(row['Size'])
        row['Price_Clean'] = self.clean_price(row['Price'])
        row['Installs_Clean'] = self.clean_installs(row['Installs'])
        row['Android_Ver_Clean'] = self.clean_android_version(row['Android Ver'])
        return row

    def clean_apps_dataset(self, df: pd.DataFrame) -> Tuple[pd.DataFrame, CleaningReport]:
        """Pulisce il dataset delle applicazioni"""
        logger.info("Pulizia dataset applicazioni in corso...")
        df_clean = df.copy()
        original_rows = len(df_clean)
        missing_before = df_clean.isnull().sum().to_dict()

        # Pulizia parallela delle righe
        with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
            cleaned_rows = list(executor.map(self._parallel_clean_row, [row for _, row in df_clean.iterrows()]))
        df_clean = pd.DataFrame(cleaned_rows, index=df_clean.index)

        # Conversione dei tipi di dato
        df_clean['Rating'] = pd.to_numeric(df_clean['Rating'], errors='coerce')
        df_clean['Reviews'] = pd.to_numeric(df_clean['Reviews'], errors='coerce')
        df_clean['Last Updated'] = pd.to_datetime(df_clean['Last Updated'], errors='coerce')

        # Rimozione righe con valori mancanti critici
        df_clean = df_clean.dropna(subset=['Android_Ver_Clean'])

        # Feature engineering
        df_clean['Days_Since_Update'] = (pd.Timestamp.now() - df_clean['Last Updated']).dt.days

        # Categorizzazione
        df_clean['Price_Category'] = pd.cut(
            df_clean['Price_Clean'],
            bins=[-np.inf, 0, 0.99, 2.99, 4.99, np.inf],
            labels=['Free', 'Very Low', 'Low', 'Medium', 'Premium']
        )

        df_clean['Install_Category'] = pd.cut(
            df_clean['Installs_Clean'],
            bins=[0, 1000, 100000, 1000000, 10000000, np.inf],
            labels=['Very Low', 'Low', 'Medium', 'High', 'Very High']
        )

        # Calcolo metriche di mercato
        total_installs = df_clean['Installs_Clean'].sum()
        df_clean['Market_Share'] = df_clean['Installs_Clean'] / total_installs

        df_clean['Category_Share'] = df_clean.groupby('Category')['Installs_Clean'].transform(
            lambda x: x / x.sum()
        )

        # Report pulizia
        cleaning_report = CleaningReport(
            original_rows=original_rows,
            cleaned_rows=len(df_clean),
            removed_rows=original_rows - len(df_clean),
            missing_before=missing_before,
            missing_after=df_clean.isnull().sum().to_dict(),
            cleaning_steps=self.cleaning_steps
        )

        return df_clean, cleaning_report

class ReviewsDataCleaner(DataCleaner):

    def clean_reviews_dataset(self, df: pd.DataFrame) -> Tuple[pd.DataFrame, CleaningReport]:
        """Pulisce il dataset delle recensioni"""
        logger.info("Pulizia dataset recensioni in corso...")
        df_clean = df.copy()
        original_rows = len(df_clean)
        missing_before = df_clean.isnull().sum().to_dict()

        # Pulizia valori mancanti
        df_clean = df_clean.dropna(subset=['Sentiment', 'Sentiment_Polarity'])

        # Conversione tipi di dato
        df_clean['Sentiment_Polarity'] = pd.to_numeric(df_clean['Sentiment_Polarity'], errors='coerce')
        df_clean['Sentiment_Subjectivity'] = pd.to_numeric(df_clean['Sentiment_Subjectivity'], errors='coerce')

        # Feature engineering
        df_clean['Review_Length'] = df_clean['Translated_Review'].str.len()

        # Categorizzazione sentiment
        df_clean['Sentiment_Category'] = pd.cut(
            df_clean['Sentiment_Polarity'],
            bins=[-1, -0.33, 0.33, 1],
            labels=['Negative', 'Neutral', 'Positive']
        )

        # Report pulizia
        cleaning_report = CleaningReport(
            original_rows=original_rows,
            cleaned_rows=len(df_clean),
            removed_rows=original_rows - len(df_clean),
            missing_before=missing_before,
            missing_after=df_clean.isnull().sum().to_dict(),
            cleaning_steps=[]
        )

        return df_clean, cleaning_report

def clean_datasets(apps_df: pd.DataFrame,
                  reviews_df: pd.DataFrame,
                  apps_cleaner: Optional[AppsDataCleaner] = None,
                  reviews_cleaner: Optional[ReviewsDataCleaner] = None) -> Tuple[pd.DataFrame, pd.DataFrame]:
    logger.info("=== INIZIO PROCESSO DI PULIZIA ===")

    # Inizializzazione cleaners se non forniti
    apps_cleaner = apps_cleaner or AppsDataCleaner()
    reviews_cleaner = reviews_cleaner or ReviewsDataCleaner()

    try:
        # Pulizia dataset app
        apps_clean, apps_report = apps_cleaner.clean_apps_dataset(apps_df)
        logger.info(f"\nPulizia apps dataset completata:")
        logger.info(f"Righe originali: {apps_report.original_rows:,}")
        logger.info(f"Righe dopo pulizia: {apps_report.cleaned_rows:,}")
        logger.info(f"Righe rimosse: {apps_report.removed_rows:,}")

        # Pulizia dataset recensioni
        reviews_clean, reviews_report = reviews_cleaner.clean_reviews_dataset(reviews_df)
        logger.info(f"\nPulizia reviews dataset completata:")
        logger.info(f"Righe originali: {reviews_report.original_rows:,}")
        logger.info(f"Righe dopo pulizia: {reviews_report.cleaned_rows:,}")
        logger.info(f"Righe rimosse: {reviews_report.removed_rows:,}")

        return apps_clean, reviews_clean

    except Exception as e:
        logger.error(f"Errore durante la pulizia dei dati: {str(e)}")
        raise

# Esecuzione della pulizia
apps_clean, reviews_clean = clean_datasets(apps_df, reviews_df)

4. Analisi esplorativa dei dati¶

Questa cella di codice si concentra sull'analisi esplorativa dei dati puliti delle app per ottenere informazioni utili sul mercato ed in essa vengono definite classi e funzioni per eseguire analisi statistiche e generare visualizzazioni. La prima parte del codice definisce due classi utilizzando il decoratore @dataclass:

  1. CategoryStats: classe utilizzata per memorizzare le informazioni statistiche sulle categorie di app. Include campi come il numero di app in una categoria, il rating medio, la percentuale di app a pagamento, il numero medio di installazioni e la dimensione media delle app;
  2. MarketAnalysis: classe utilizzata per contenere i risultati dell'analisi di mercato. Include un dataframe pandas con le statistiche per categoria, un dizionario con le statistiche relative ai prezzi, un dataframe pandas con l'analisi della competitività del mercato e una lista per memorizzare le figure plotly generate dall'analisi.

 

La classe principale di questa sezione è MarketAnalyzer, che prende come input il dataframe apps_df pulito durante l'inizializzazione. La classe include metodi per calcolare statistiche, creare visualizzazioni ed eseguire diversi tipi di analisi di mercato. Il metodo __init__ inizializza MarketAnalyzer filtrando le righe in cui Category è '1.9' e imposta il numero di thread che verranno usati per i calcoli in parallelo tramite l'argomento max_workers.

Il metodo _calculate_category_stats è un metodo helper, decorato con @lru_cache(maxsize=None), che calcola e restituisce CategoryStats per una data categoria. Il decoratore @lru_cache memorizza i risultati di questo metodo per efficienza, evitando calcoli ridondanti quando viene chiamato più volte con la stessa categoria.

Il metodo analyze_category_distribution analizza la distribuzione delle app tra le diverse categorie. Utilizza un ThreadPoolExecutor per parallelizzare il calcolo delle statistiche per categoria, crea un dataframe pandas con le statistiche calcolate e genera un grafico a barre usando plotly.express per visualizzare la distribuzione delle app tra le categorie, usando il rating medio come sfumatura di colore.

Il metodo analyze_price_distribution analizza invece la distribuzione dei prezzi delle app a pagamento. Filtra apps_df per includere solo le app a pagamento, definisce gli intervalli di prezzo e le relative etichette per raggruppare i prezzi in categorie, calcola le statistiche dei prezzi (come il numero totale di app, il numero di app a pagamento, la percentuale di app a pagamento e il prezzo medio, mediano e massimo delle app a pagamento), aggrega i dati per intervallo di prezzo e genera un grafico a barre con plotly.graph_objects che ha lo scopo di mostrare la distribuzione dei prezzi usando la percentuale di app per ciascun intervallo di prezzo sull'asse y e gli intervalli di prezzo sull'asse x. Le barre sono colorate in base al rating medio all'interno di ciascun intervallo di prezzo.

Il metodo analyze_market_competition ha l'obiettivo di rappresentare visivamente la competitività del mercato delle app attraverso una mappa che mette in relazione diverse metriche chiave. Per costruire questa mappa, il codice inizia raggruppando il dataframe apps_df per categoria ("Category") e calcolando per ciascuna di essa quattro metriche fondamentali:

  • il numero di app che, in questo caso, indica direttamente il livello di competizione;
  • il rating medio, che riflette la qualità percepita dagli utenti;
  • la percentuale di app a pagamento, che rappresenta la propensione al pagamento in quella categoria;
  • il numero medio di installazioni, che offre una misura dell'ampiezza del mercato.

La propensione al pagamento (paid_perc) viene calcolata con un'espressione lambda 'Price_Clean': lambda x: (x > 0).mean() * 100. Questa formula trasforma i prezzi delle app in valori booleani (True per app a pagamento, False per app gratuite), ne calcola la media (ottenendo la proporzione di app a pagamento) e moltiplica per 100 per esprimere il risultato in percentuale.

Nello specifico:

  1. (x > 0) crea un array di valori booleani dove True rappresenta le app con prezzo maggiore di zero (app a pagamento) e False quelle gratuite;
  2. .mean() calcola la media di questi valori booleani, che equivale alla proporzione di app a pagamento (un valore tra 0 e 1);
  3. * 100 converte questa proporzione in una percentuale.

Questa metrica è importante perché fornisce un'indicazione della disponibilità degli utenti di quella categoria a pagare per le app. Una percentuale più alta suggerisce che:

  • gli utenti in quella categoria sono più disposti a pagare per contenuti e funzionalità;
  • esiste un precedente di monetizzazione diretta che nuovi entranti potrebbero sfruttare;
  • il modello di business "premium" (pagamento diretto) potrebbe essere più facilmente accettato rispetto a modelli freemium o basati sulla pubblicità.

Successivamente, viene calcolato un "indice di dimensione del mercato" (market_size_index) basato sul numero di app in ogni categoria, normalizzato tra 0 e 1. Questo indice rappresenta la dimensione relativa di ciascuna categoria rispetto alle altre, dove una categoria con più app avrà un indice più alto, indicando un mercato più grande e potenzialmente più competitivo.

La visualizzazione vera e propria è realizzata con un grafico a dispersione, creato con plotly.graph_objects. Ogni punto sul grafico rappresenta una categoria di app, posizionata secondo due dimensioni principali:

  • l'asse x rappresenta il numero di app nella categoria, ovvero il livello di competizione. Più a destra si trova un punto, maggiore sarà il numero di app in quella categoria e quindi più alta la competizione;
  • l'asse y rappresenta il rating medio delle app nella categoria. Più in alto si trova un punto, maggiore sarà la qualità percepita delle app in quella categoria.

La dimensione di ciascun punto è proporzionale all'indice di dimensione del mercato calcolato in precedenza. Quindi, punti più grandi indicano categorie con un mercato potenzialmente più ampio. Il colore di ciascun punto rappresenta il rating medio delle app in quella categoria, usando una scala di colori dal rosso (rating basso) al verde (rating alto), fornendo una ridondanza visiva che rafforza l'informazione dell'asse y e permette di identificare rapidamente le categorie con app percepite come di qualità superiore.

Infine, il metodo calcola un opportunity_score per ciascuna categoria, basato su una combinazione ponderata di tre fattori:

  • 40% dal rating medio, premiando le categorie con app di qualità superiore;
  • 30% dall'inverso dell'indice di dimensione del mercato, favorendo le categorie meno competitive;
  • 30% dalla percentuale di app a pagamento, valorizzando le categorie dove esiste una cultura dell'acquisto.

L'idea alla base di questo punteggio è che le categorie con alto rating, bassa competizione e alta percentuale di app a pagamento rappresentano potenzialmente le migliori opportunità di mercato, andando a bilanciare qualità, facilità di ingresso e potenziale di monetizzazione diretta. La visualizzazione è stata ulteriormente arricchita con un'annotazione che mostra le migliori 5 categorie in base a questo opportunity score.

 

La funzione perform_exploratory_analysis gestisce l'intero processo di analisi esplorativa dei dati. Inizializza MarketAnalyzer se non viene fornito negli argomenti della funzione, chiama i metodi di analisi di MarketAnalyzer per ottenere i risultati e le relative visualizzazioni, calcola le prime 5 opportunità di mercato in base all'opportunity score e restituisce un oggetto MarketAnalysis contenente i risultati e tutte i grafici generati.

Infine, il codice esegue l'analisi esplorativa chiamando perform_exploratory_analysis - utilizzando i dataframe puliti apps_clean e reviews_clean - e visualizza le figure generate iterando attraverso le figures nell'oggetto market_analysis e usando il metodo show() per visualizzare ciascun grafico plotly nell'output.

 

Interpretazione dei risultati¶

 

Distribuzione delle app per categoria e rating medio¶

Il primo grafico offre una panoramica sulla distribuzione delle app nel Google Play Store. Ciò che colpisce immediatamente è la predominanza della categoria "FAMILY", che ospita quasi 2000 applicazioni, rappresentando il segmento di mercato più ampio. Al secondo posto troviamo "GAME" con più di 1000 app, che riflette comunque la significativa popolarità del gaming mobile e la sua capacità di generare ricavi importanti. Più distanziata troviamo "TOOLS" con circa 750 app, una categoria che racchiude utility e strumenti di vario genere.

La visualizzazione rivela un ecosistema di app estremamente disomogeneo, dove poche categorie raccolgono la maggior parte delle applicazioni, mentre molte altre rappresentano nicchie con una presenza numerica decisamente più contenuta. Categorie come "EVENTS", "BEAUTY", "PARENTING" e "WEATHER" hanno una presenza molto limitata, suggerendo potenziali opportunità in mercati meno saturi.

Piuttosto interessante è la relazione tra numero di app e valutazione (rating) media, evidenziata dal sistema di colorazione. Le categorie con un minor numero di applicazioni tendono ad avere rating medi più elevati (visualizzati in verde), come nel caso di "EVENTS", "EDUCATION" e "BOOKS_AND_REFERENCE". Questo fenomeno potrebbe indicare che in mercati meno affollati è più facile emergere con prodotti di qualità che soddisfano più facilmente le aspettative degli utenti. Al contrario, categorie molto competitive come "DATING" e "CASINO" mostrano rating mediamente più bassi (in arancione-rosso), suggerendo una maggiore difficoltà nel distinguersi e soddisfare pienamente le aspettative degli utenti in mercati tendenzialmente saturi.

 

Distribuzione dei prezzi delle app a pagamento¶

Il secondo grafico ci porta a esplorare le strategie di monetizzazione diretta attraverso la distribuzione dei prezzi delle app a pagamento. Il mercato mostra una chiara preferenza per il pricing nella fascia 1-2.99$, che raccoglie nientemeno che il 36.4% delle app a pagamento. Questo dato suggerisce un punto di equilibrio tra l'accessibilità per gli utenti e il valore percepito da parte degli sviluppatori.

È interessante notare come la distribuzione non segua un andamento lineare decrescente: la fascia 0-1$ (20.2%) è più popolata della fascia 3-4.99$ (19.9%), mentre c'è un netto calo nella fascia 5-9.99$ (11.3%) prima di una leggera risalita nella categoria premium oltre i 10$ (12.1%). Questo pattern riflette le diverse strategie di monetizzazione e posizionamento: molti sviluppatori optano per prezzi molto bassi puntando sul volume, mentre altri scelgono un posizionamento premium con prezzi elevati mirando a utenti disposti a pagare per funzionalità esclusive o molto specifiche.

La colorazione delle barre nel grafico offre un'ulteriore dimensione analitica. Esaminando le sfumature cromatiche, emerge che le applicazioni nelle fasce di prezzo inferiori presentano valutazioni mediamente superiori, evidenziando una corrispondenza efficace tra il valore economico richiesto e la soddisfazione dell′utenza. Questo fenomeno suggerisce che le aspettative dei consumatori a questi livelli di prezzo sono generalmente soddisfatte dall′esperienza d′uso. Per contro, le applicazioni posizionate nella fascia premium (oltre 10$) mostrano valutazioni mediamente inferiori, il che potrebbe indicare che gli utenti sono più critici quando pagano prezzi premium e le loro aspettative sono più difficili da soddisfare.

 

Mappa competitiva del mercato¶

La mappa competitiva rappresenta uno strumento analitico sofisticato che offre una visione multidimensionale del mercato delle app. In questo grafico a dispersione, ogni categoria viene posizionata in base a due metriche fondamentali: il numero di applicazioni (asse X) che indica il livello di competizione, e il rating medio (asse Y) che riflette la soddisfazione degli utenti.

Il panorama competitivo si presenta stratificato, con "FAMILY" che domina in termini quantitativi con circa 2000 app (estrema destra del grafico), seguito da "GAME" con circa 1000 app e "TOOLS" con circa 750 app, come già risultava evidente dal grafico "Distribuzione delle app per categoria e rating medio" La dimensione dei cerchi, proporzionale all'indice di dimensione del mercato, amplifica visivamente questa gerarchia, evidenziando il peso significativo di queste categorie nell'ecosistema complessivo.

Particolarmente interessante è la distribuzione verticale: categorie come "EVENTS", "EDUCATION" e "ART_AND_DESIGN" si collocano nella parte superiore del grafico con rating medi superiori a 4.3, suggerendo un'elevata qualità percepita. All'estremo opposto, categorie come "DATING" mostrano rating più contenuti, indice di una maggiore difficoltà nel soddisfare le aspettative degli utenti.

L'analisi si arricchisce significativamente grazie al riquadro informativo presente in basso a destra nel grafico, che rivela le migliori 5 opportunità di mercato secondo l'opportunity score:

  1. MEDICAL emerge come l'opportunità più promettente con uno score di 8.67, combinando un buon rating (4.2), una competizione moderata (439 app) e un'elevata propensione al pagamento (22.6% di app a pagamento);
  2. PERSONALIZATION si posiziona immediatamente dopo con 8.63, grazie a un rating leggermente superiore (4.3) e un mercato meno affollato (352 app), mantenendo un'alta percentuale di app a pagamento (22.2%);
  3. BOOKS_AND_REFERENCE presenta uno score di 6.06, con un eccellente rating (4.3) e bassa competizione (200 app), sebbene presenti una propensione al pagamento inferiore (13.5%);
  4. WEATHER rappresenta un'interessante nicchia con uno score di 5.15, combinando un buon rating (4.2) con una competizione minima (solo 57 app) e una discreta propensione al pagamento (10.5%);
  5. TOOLS, nonostante l'elevata competizione (744 app), mantiene uno score rispettabile di 4.57 grazie ad rating discreto (4.0) e alla presenza di un buon segmento di utenti disposti a pagare (9.3%).

Questa classifica rivela come le migliori opportunità non siano necessariamente le categorie situate nell'angolo in alto a sinistra del grafico (alto rating, bassa competizione). La propensione al pagamento gioca un ruolo importante, rendendo categorie come MEDICAL e PERSONALIZATION particolarmente attraenti nonostante non siano le meno competitive o quelle con le valutazioni più alte in assoluto.

In [ ]:
logger = logging.getLogger(__name__)

@dataclass
class CategoryStats:
    num_apps: int
    avg_rating: float
    paid_perc: float
    avg_installs: float
    avg_size: float

@dataclass
class MarketAnalysis:
    category_stats: pd.DataFrame
    price_stats: Dict[str, float]
    market_analysis: pd.DataFrame
    figures: List[go.Figure] = field(default_factory=list)

class MarketAnalyzer:

    def __init__(self, apps_df: pd.DataFrame, max_workers: int = 4):
        self.apps_df = apps_df[apps_df['Category'] != '1.9'].copy()
        self.max_workers = max_workers

    @lru_cache(maxsize=None)
    def _calculate_category_stats(self, category: str) -> CategoryStats:
        cat_data = self.apps_df[self.apps_df['Category'] == category]
        return CategoryStats(
            num_apps=len(cat_data),
            avg_rating=cat_data['Rating'].mean(),
            paid_perc=(cat_data['Price_Clean'] > 0).mean() * 100,
            avg_installs=cat_data['Installs_Clean'].mean(),
            avg_size=cat_data['Size_MB'].mean()
        )

    def analyze_category_distribution(self) -> Tuple[pd.DataFrame, go.Figure]:
        # Calcolo parallelo delle statistiche per categoria
        categories = self.apps_df['Category'].unique()
        with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
            stats = list(executor.map(self._calculate_category_stats, categories))

        # Creazione DataFrame
        category_stats = pd.DataFrame({
            'Category': categories,
            'num_apps': [s.num_apps for s in stats],
            'avg_rating': [s.avg_rating for s in stats],
            'paid_perc': [s.paid_perc for s in stats],
            'avg_installs': [s.avg_installs for s in stats],
            'avg_size': [s.avg_size for s in stats]
        }).round(2)

        # Creazione grafico ottimizzato
        fig = px.bar(
            category_stats,
            x='Category',
            y='num_apps',
            color='avg_rating',
            title='Distribuzione delle app per categoria e rating medio',
            labels={
                'num_apps': 'Numero di app',
                'Category': 'Categoria',
                'avg_rating': 'Rating medio'
            },
            color_continuous_scale=[[0, '#B30000'], [0.4, '#FF0000'],
                                  [0.6, '#FFA500'], [0.75, '#2ECC40'],
                                  [1, '#00B300']],
            range_color=[3.2, 4.8]
        )

        fig.update_layout(
            xaxis_tickangle=-45,
            showlegend=True,
            height=600,
            title_x=0.5,
            font=dict(family="Arial", size=12),
            margin=dict(t=100, l=50, r=50, b=100)
        )

        return category_stats.set_index('Category'), fig

    def analyze_price_distribution(self) -> Tuple[Dict[str, float], go.Figure]:
        df_paid = self.apps_df[self.apps_df['Price_Clean'] > 0].copy()

        price_ranges = [0, 1, 2.99, 4.99, 9.99, float('inf')]
        price_labels = ['0-1$', '1-2.99$', '3-4.99$', '5-9.99$', '10$+']

        df_paid['price_range'] = pd.cut(df_paid['Price_Clean'],
                                      bins=price_ranges,
                                      labels=price_labels)

        # Calcolo statistiche prezzi
        price_stats = {
            'total_apps': len(self.apps_df),
            'paid_apps': len(df_paid),
            'paid_percentage': (len(df_paid) / len(self.apps_df)) * 100,
            'avg_price': df_paid['Price_Clean'].mean(),
            'median_price': df_paid['Price_Clean'].median(),
            'max_price': df_paid['Price_Clean'].max()
        }

        # Aggregazione dati per il grafico
        price_distribution = df_paid.groupby('price_range').agg({
            'App': 'count',
            'Rating': 'mean'
        }).reset_index()

        price_distribution['percentage'] = (
            price_distribution['App'] / len(df_paid)
        ) * 100

        # Creazione grafico
        fig = go.Figure(data=[
            go.Bar(
                x=price_distribution['price_range'],
                y=price_distribution['percentage'],
                marker=dict(
                    color=price_distribution['Rating'],
                    colorscale=[[0, '#B30000'], [0.4, '#FF0000'],
                               [0.6, '#FFA500'], [0.75, '#2ECC40'],
                               [1, '#00B300']],
                    colorbar=dict(
                        title="Rating medio",
                        titleside="right",
                        xpad=30,
                        len=0.9,
                        thickness=20
                    ),
                    cmin=3.2,
                    cmax=4.8
                ),
                text=price_distribution['percentage'].round(1).astype(str) + '%',
                textposition='outside'
            )
        ])

        fig.update_layout(
            title='Distribuzione dei prezzi delle app a pagamento',
            title_x=0.5,
            xaxis_title='Fascia di prezzo',
            yaxis_title='Percentuale di app (%)',
            height=500,
            yaxis_range=[0, max(price_distribution['percentage']) * 1.1],
            bargap=0.2,
            font=dict(family="Arial", size=12)
        )

        return price_stats, fig

    def analyze_market_competition(self) -> Tuple[pd.DataFrame, go.Figure]:
        """Analizza la competitività del mercato"""
        market_analysis = self.apps_df.groupby('Category').agg({
            'App': 'count',
            'Rating': 'mean',
            'Price_Clean': lambda x: (x > 0).mean() * 100,
            'Installs_Clean': 'mean'
        }).round(2)

        market_analysis.columns = ['num_apps', 'avg_rating', 'paid_perc', 'avg_installs']

        # Calcolo indice dimensione mercato
        market_analysis['market_size_index'] = (
            (market_analysis['num_apps'] - market_analysis['num_apps'].min()) /
            (market_analysis['num_apps'].max() - market_analysis['num_apps'].min())
        )

        # Creazione grafico
        fig = go.Figure(data=[
            go.Scatter(
                x=market_analysis['num_apps'],
                y=market_analysis['avg_rating'],
                mode='markers+text',
                text=market_analysis.index,
                textposition='top center',
                marker=dict(
                    size=market_analysis['market_size_index'] * 50,
                    color=market_analysis['avg_rating'],
                    colorscale=[[0, '#B30000'], [0.4, '#FF0000'],
                               [0.6, '#FFA500'], [0.75, '#2ECC40'],
                               [1, '#00B300']],
                    colorbar=dict(
                        title="Rating medio",
                        titleside="right",
                        xpad=30,
                        len=0.9,
                        thickness=20
                    ),
                    cmin=3.2,
                    cmax=4.8
                )
            )
        ])

        fig.update_layout(
            title='Mappa competitiva del mercato',
            title_x=0.5,
            xaxis_title='Numero di app (competizione)',
            yaxis_title='Rating medio',
            height=600,
            showlegend=False,
            font=dict(family="Arial", size=12)
        )

        # Calcolo opportunity score
        market_analysis['opportunity_score'] = (
            market_analysis['avg_rating'] * 0.4 +
            (1 - market_analysis['market_size_index']) * 0.3 +
            market_analysis['paid_perc'] * 0.3
        )

        # Aggiungi annotazione con migliori opportunità
        top_opportunities = market_analysis.nlargest(5, 'opportunity_score')
        top_text = "<b>Top 5 opportunità di mercato:</b><br>"
        for cat in top_opportunities.index:
            score = market_analysis.loc[cat, 'opportunity_score']
            rating = market_analysis.loc[cat, 'avg_rating']
            apps = market_analysis.loc[cat, 'num_apps']
            paid = market_analysis.loc[cat, 'paid_perc']
            top_text += f"<b>{cat}</b>: score {score:.2f} (rating {rating:.1f}, app {apps}, propensione {paid:.1f}%)<br>"

        fig.add_annotation(
            x=0.99,
            y=0.01,
            xref="paper",
            yref="paper",
            xanchor="right",
            yanchor="bottom",
            text=top_text,
            showarrow=False,
            font=dict(size=11),
            align="left",
            bgcolor="rgba(255, 255, 255, 0.9)",
            bordercolor="black",
            borderwidth=1,
            borderpad=6
        )

        return market_analysis, fig

def perform_exploratory_analysis(apps_df: pd.DataFrame,
                               reviews_df: Optional[pd.DataFrame] = None,
                               analyzer: Optional[MarketAnalyzer] = None) -> MarketAnalysis:
    logger.info("=== INIZIO ANALISI ESPLORATIVA ===")

    analyzer = analyzer or MarketAnalyzer(apps_df)
    figures = []

    try:
        # 1. Analisi distribuzione per categoria
        logger.info("\n1. Analisi distribuzione per categoria")
        category_stats, category_fig = analyzer.analyze_category_distribution()
        figures.append(category_fig)

        # 2. Analisi prezzi
        logger.info("\n2. Analisi distribuzione prezzi")
        price_stats, price_fig = analyzer.analyze_price_distribution()
        figures.append(price_fig)

        # 3. Analisi competitiva
        logger.info("\n3. Analisi competitiva del mercato")
        market_analysis, market_fig = analyzer.analyze_market_competition()
        figures.append(market_fig)

        # Log risultati principali
        logger.info("\nTop 5 opportunità di mercato:")
        top_opportunities = market_analysis.nlargest(5, 'opportunity_score')
        for cat in top_opportunities.index:
            logger.info(f"\n{cat}:")
            logger.info(f"- Score: {market_analysis.loc[cat, 'opportunity_score']:.2f}")
            logger.info(f"- Rating medio: {market_analysis.loc[cat, 'avg_rating']:.2f}")
            logger.info(f"- Competizione: {market_analysis.loc[cat, 'num_apps']:,} app")
            logger.info(f"- % app a pagamento: {market_analysis.loc[cat, 'paid_perc']:.1f}%")

        return MarketAnalysis(
            category_stats=category_stats,
            price_stats=price_stats,
            market_analysis=market_analysis,
            figures=figures
        )

    except Exception as e:
        logger.error(f"Errore durante l'analisi esplorativa: {str(e)}")
        raise

# Esecuzione dell'analisi esplorativa
market_analysis = perform_exploratory_analysis(apps_clean, reviews_clean)

# Visualizzazione dei grafici
for fig in market_analysis.figures:
    fig.show()

Analisi delle performance e delle metriche chiave¶

La quinta cella rappresenta il passaggio da un'esplorazione descrittiva iniziale a un'indagine più approfondita delle relazioni tra variabili e dei trend temporali. Se nei blocchi precedenti sono state identificate le opportunità di mercato in base alle categorie, ora esploriamo come diverse metriche si influenzano reciprocamente e come il mercato si è evoluto nel tempo.

Vengono definite due classi NamedTuple che fungono da contenitori tipizzati per i risultati: CorrelationResults raccoglie le matrici di correlazione di Pearson e Spearman insieme alle loro visualizzazioni, mentre TimeMetrics contiene i dati temporali aggregati e il grafico corrispondente.

La classe principale PlayStoreAnalyzer è implementata come @dataclass e nel suo metodo __post_init__ viene eseguita un'operazione di filtraggio necessaria:

self.apps_df = self.apps_df[self.apps_df['Category'] != '1.9'].copy()

Questa riga rimuove dal dataset un'osservazione chiaramente anomala in cui la colonna "Category" contiene il valore '1.9', che non corrisponde a nessuna categoria legittima del Google Play Store. Andando ad analizzare più attentamente il dataset si può notare che si tratta di un record con un disallineamento dei dati, dove i valori sono stati spostati di posizione tra le colonne. In questa riga, infatti, troviamo valori come un rating impossibile di "19", la stringa "Free" nella colonna delle installazioni e altri elementi chiaramente fuori posto. Ho quindi deciso di rimuovere completamente questi dati che comprometterebbero l'affidabilità delle analisi successive.

Dopo questa pulizia iniziale, il metodo prepare_data() trasforma i dati grezzi in metriche analitiche significative attraverso diverse operazioni essenziali:

  1. elabora le informazioni temporali convertendo la colonna "Last Updated" in formato datetime, calcolando il numero di giorni trascorsi dall'ultimo aggiornamento rispetto alla data più recente nel dataset ed estraendo l'anno di aggiornamento in una nuova colonna;

  2. applica trasformazioni logaritmiche (np.log1p())alle colonne delle installazioni e delle dimensioni. Questa tecnica è particolarmente utile per gestire distribuzioni asimmetriche o con ampi intervalli di valori, permettendo pertanti di visualizzare e analizzare meglio relazioni che altrimenti sarebbero difficili da interpretare.

Il codice calcola anche metriche di mercato come la quota di mercato globale (dividendo le installazioni di ogni app per il totale delle installazioni) e la quota all'interno della categoria, utilizzando la funzione transform di pandas che permette di mantenere la dimensionalità originale del dataframe.

Inoltre, il metodo prevede l'integrazione dei dati di sentiment nel dataset principale attraverso _merge_sentiment_data(). Questo processo avviene in tre passaggi: prima unisce i dati delle recensioni con le categorie delle app tramite un inner join, poi aggrega i dati per calcolare la polarità media (positività/negatività) e la soggettività media delle recensioni per ciascuna app e infine unisce questi dati aggregati al dataframe principale con un left join.

Ci tengo comunque anche in questo caso a specificare che metriche di sentiment (Sentiment_Polarity e Sentiment_Subjectivity) non vengono effettivamente utilizzate nelle visualizzazioni e nelle analisi di correlazione per le motivazioni espresse precedentemente.

 

Il metodo analyze_correlations() calcola due tipi di coefficienti di correlazione che forniscono prospettive complementari:

  • la correlazione di Pearson (r = Σ[(x_i - x̄)(y_i - ȳ)] / √[Σ(x_i - x̄)² · Σ(y_i - ȳ)²] - in cui x_i e y_i sono le osservazioni individuali e x̄ e ȳ sono le medie delle variabili) misura relazioni lineari tra variabili, quantificando quanto due variabili tendono ad aumentare o diminuire insieme in modo proporzionale. È particolarmente efficace per relazioni lineari, ma può essere fuorviante in presenza di relazioni non lineari o outlier significativi;

  • la correlazione di Spearman (ρ = 1 - (6 · Σd_i²) / [n(n² - 1)] - in cui d_i è la differenza tra i ranghi delle osservazioni corrispondenti e n è il numero di osservazioni) cattura relazioni monotoniche - ovvero quando una variabile aumenta, l'altra tende a cambiare sempre nella stessa direzione - anche quando non sono strettamente lineari ed è basata sui ranghi delle variabili anziché sui loro valori assoluti. Essendo calcolata sui ranghi, risulta più robusta rispetto ad outlier e distribuzioni non normali, fornendo pertanto una visione più completa quando si analizzano dati come quelli in questione che spesso presentano distribuzioni asimmetriche.

Il calcolo delle correlazioni viene eseguito utilizzando le funzionalità integrate di pandas:

# Calcolo correlazioni
data = self.apps_df[metrics.keys()]
pearson_corr = data.corr(method='pearson').round(3)
spearman_corr = data.corr(method='spearman').round(3)

Il metodo round(3) arrotonda i coefficienti di correlazione a tre decimali per migliorare la leggibilità.

L'utilizzo di entrambe le metriche offre una visione potenzialmente più completa delle relazioni tra variabili, permettendo di identificare sia pattern lineari che non lineari nei dati.

Per visualizzare queste correlazioni, il codice crea due rappresentazioni principali. La prima è una heatmap che confronta affiancate le matrici di correlazione di Pearson e Spearman, utilizzando una scala di colori dal rosso (correlazioni negative) al blu (correlazioni positive) per evidenziare visivamente la forza e la direzione delle relazioni. La seconda è una matrice di scatter plot che mostra le relazioni tra coppie specifiche di variabili. Nel caso dello scatter plot che analizza la relazione tra rating e giorni dall'ultimo aggiornamento il codice aggiunge un leggero rumore casuale ai valori di rating ("jitter") per evitare sovrapposizioni di punti, calcola poi una media mobile per evidenziare la tendenza generale e limita l'asse y al 95° percentile per evitare che valori estremi comprimano la visualizzazione. In questa seconda visualizzazione, il colore dei punti è basato sul rating delle app, con una scala che va dal rosso (rating bassi) al verde (rating alti), permettendo di identificare facilmente pattern nelle relazioni tra variabili.

 

Il metodo analyze_temporal_trends() offre una prospettiva evolutiva del mercato delle app, aggregando i dati per anno di aggiornamento per comprendere come le metriche chiave sono cambiate nel tempo. L'analisi inizia calcolando il prezzo medio delle app a pagamento per ogni anno, escludendo le app gratuite per non distorcere i risultati. Vengono poi aggregati i dati per anno, calcolando il rating medio, la dimensione media, le installazioni medie e il conteggio delle app per ogni periodo.

La visualizzazione dell'evoluzione temporale adotta un approccio a due subplot che organizzano le metriche in gruppi concettualmente coerenti:

  • il subplot superiore presenta le metriche di prodotto, caratteristiche intrinseche delle app: rating medio (che riflette la qualità percepita dagli utenti), prezzo medio (che indica le strategie di monetizzazione) e dimensione media (che, in teoria, può essere correlata alla complessità e ricchezza delle funzionalità). Questi tre parametri vengono visualizzati con linee di colori diversi per facilitarne la distinzione: verde per il rating, rosso per il prezzo e blu per la dimensione;

  • il subplot inferiore visualizza invece le metriche di mercato, indicatori della performance e della diffusione delle app: installazioni medie (rappresentate da una linea arancione che mostra la popolarità media delle app) e numero totale di app (visualizzato come barre grigie semitrasparenti, che forniscono contesto sull'evoluzione della dimensione del mercato).

Per il subplot inferiore viene implementata una scala logaritmica sull'asse Y, che trasforma incrementi esponenziali in incrementi lineari: ad esempio, i passaggi da 1000 a 10000, da 10000 a 100000 e da 100000 a 1000000 appaiono come distanze uguali nel grafico. Questo approccio permette di visualizzare contemporaneamente app con popolarità molto diverse e apprezzare cambiamenti proporzionali anziché assoluti, rivelando pattern di crescita relativi che altrimenti rimarrebbero nascosti in una scala lineare tradizionale.

Infine il metodo _format_trend_value()personalizza il formato in base al tipo di metrica: trasforma grandi numeri di installazioni in formati più leggibili (K per migliaia, M per milioni), formatta i prezzi con il simbolo del dollaro, aggiunge "MB" alle dimensioni e gestisce in modo appropriato i valori mancanti con la notazione "N/D".

 

La funzione analyze_play_store() coordina il processo di analisi. La sua implementazione segue diverse fasi:

  1. la funzione crea un'istanza di PlayStoreAnalyzer passando i dataframe delle app e delle recensioni. Questa istanza contiene tutta la logica specializzata per le diverse analisi. Predispone inoltre due strutture dati vuote: una lista figures per raccogliere le visualizzazioni generate e un dizionario results per memorizzare i risultati numerici.

  2. esegue l'analisi delle correlazioni chiamando il metodo analyze_correlations() all'interno di un blocco try-except per la gestione degli errori. I risultati di questa analisi vengono memorizzati nel dizionario results sotto la chiave 'correlations' e le figure generate vengono aggiunte alla lista figures. Una caratteristica importante è l'identificazione e la registrazione delle correlazioni statisticamente rilevanti: la funzione itera attraverso le matrici di correlazione di Pearson e Spearman, estraendo e loggando solo le relazioni con coefficiente di correlazione superiore a 0.3 in valore assoluto.

  3. procede poi con l'analisi dei trend temporali chiamando analyze_temporal_trends(). Anche in questo caso, i risultati e le visualizzazioni vengono memorizzati. Un elemento che merita un minimo di approfondimento è il calcolo dettagliato dei cambiamenti tra il primo e l'ultimo anno disponibile nel dataset: per il rating medio viene calcolata la variazione percentuale, mentre per altre metriche come dimensione media, installazioni medie e numero di app, vengono registrati i valori assoluti all'inizio e alla fine del periodo analizzato.

  4. tutti i risultati e le figure vengono raccolti nel dizionario results, che viene restituito come output della funzione. Il blocco try-except che avvolge l'intera implementazione fornisce robustezza all'analisi:. Quando si verifica un'eccezione, il codice cattura l'errore entrando nel blocco except, lo registra nel sistema di logging attraverso logger.error() e lo ri-solleva (ovvero l'eccezione può poi continuare il suo percorso verso l'alto nella pila di chiamate) con l'istruzione raise senza parametri.

Questo approccio:

  • garantisce che nessun errore passi inosservato grazie alla registrazione nel log;

  • mantiene la traccia completa dell'errore originale (tipo, messaggio e stack trace) e permette al codice chiamante di implementare eventuali strategie di recupero.

A differenza di una gestione che "inghiottirebbe" l'errore, questa tecnica assicura che problemi nei dati o nel processo di analisi vengano correttamente identificati e possano essere affrontati in modo appropriato.

 

Interpretazione dei risultati¶

 

Confronto correlazioni (Pearson vs Spearman)¶

Analizzando le matrici, emergono diverse correlazioni interessanti:

 

Giorni dall'ultimo aggiornamento vs Log dimensione

Pearson: -0.35 / Spearman: -0.33

Questa correlazione negativa indica che app aggiornate più recentemente tendono ad avere dimensioni maggiori. Potrebbe riflettere una tendenza degli sviluppatori a rilasciare aggiornamenti che arricchiscono l'app con nuove funzionalità, aumentandone di conseguenza la dimensione.

 

Log dimensione vs Log installazioni

Pearson: 0.34 / Spearman: 0.35

Questa correlazione positiva suggerisce che app più grandi tendono ad avere più installazioni. Ciò potrebbe indicare che gli utenti sono disposti a scaricare app più pesanti quando offrono più funzionalità o contenuti, oppure che app di maggior successo tendono ad espandersi nel tempo.

 

Giorni dall'ultimo aggiornamento vs Log installazioni

Pearson: -0.19 / Spearman: -0.33

È interessante notare come questa correlazione sia più forte secondo Spearman che Pearson, suggerendo una relazione monotonica, ma non perfettamente lineare. App aggiornate più frequentemente tendono ad avere più installazioni, presumibilmente perché gli aggiornamenti regolari mantengono l'app rilevante e attraente per gli utenti.

 

Prezzo vs altre metriche

Entrambe le matrici mostrano correlazioni deboli tra il prezzo e le altre metriche, con valori che raramente superano ±0.20. Questo suggerisce che il prezzo ha una relazione limitata con gli altri parametri, il che potrebbe indicare che le strategie di prezzo sono determinate da fattori diversi dalla popolarità o dalle caratteristiche tecniche delle app.

 

Rating vs altre metriche

Anche il rating mostra correlazioni generalmente deboli con le altre metriche, con l'eccezione di una lieve correlazione negativa con i giorni dall'ultimo aggiornamento (-0.13 in Pearson, -0.19 in Spearman), suggerendo che app aggiornate più recentemente tendono ad avere valutazioni leggermente migliori.

 

Quindi riassumendo, possiamo dedurre che:

  1. la frequenza di aggiornamento sembra essere un fattore importante correlato al successo di un'app, suggerendo l'importanza di una manutenzione regolare;

  2. la dimensione dell'app ha una correlazione positiva con le installazioni, indicando che gli utenti potrebbero preferire app più ricche di funzionalità;

  3. il prezzo e il rating sembrano essere influenzati da fattori più complessi non catturati direttamente dalle altre metriche analizzate.

È tuttavia importante specificare che le correlazioni non sono estremamente forti, ma ci forniscono comunque indicazioni utili sulle relazioni tra le caratteristiche delle app.

 

Scatter plots delle relazioni principali¶

Questi scatter plots arricchiscono notevolmente la comprensione delle correlazioni analizzate precedentemente:

  1. confermano visivamente la natura delle relazioni, mostrandone non solo la forza, ma anche la forma;

  2. evidenziano pattern non lineari, particolarmente visibili nel grafico Rating vs Giorni da ultimo aggiornamento;

  3. permettono di identificare cluster e distribuzioni che le semplici correlazioni non catturano.

Le implicazioni strategiche che emergono da questa analisi visuale rafforzano quanto già osservato, ovvero:

  • gli aggiornamenti regolari sembrano essere fondamentali per mantenere rating elevati e, potenzialmente, per aumentare le installazioni

  • la relazione positiva tra dimensione delle app e loro diffusione suggerisce che gli utenti apprezzano app più ricche di funzionalità;

  • vi è un'evidente interconnessione tra rating elevato e maggior numero di installazioni, il che può indicare come la qualità percepita possa influenzare la popolarità di un'app.

 

Evoluzione temporale delle metriche chiave¶

Il grafico offre una prospettiva dinamica sul mercato delle app dal 2010 al 2018, suddivisa in due pannelli: il superiore mostra le metriche di prodotto (rating, prezzo, dimensione) e l'inferiore le metriche di mercato (installazioni e numero di app).

Nel pannello superiore osserviamo alcuni trend trend nelle caratteristiche delle app:

  • Dimensione media (linea blu): mostra la crescita più importante, passando da valori prossimi allo zero nel 2010 a circa 25 MB nel 2018. Questo incremento costante e sostanziale riflette l'evoluzione delle capacità hardware dei dispositivi mobili e la crescente complessità delle app moderne, che incorporano funzionalità sempre più avanzate, elementi grafici di maggiore qualità e contenuti multimediali.

  • Prezzo medio (linea rossa): presenta un andamento più irregolare con un'impennata notevole tra il 2016 e il 2017 (da circa 5$ a 22$), seguita da una leggera discesa nel 2018. Questo picco potrebbe indicare un cambiamento nelle strategie di monetizzazione o l'ingresso nel mercato di app premium in categorie specifiche. La successiva diminuzione suggerisce un possibile aggiustamento del mercato verso prezzi più competitivi.

  • Rating medio (linea verde): rimane notevolmente stabile intorno al valore 4 durante l'intero periodo, con variazioni minime. Questa stabilità è interessante considerando i cambiamenti significativi nelle altre metriche e suggerisce che, nonostante l'evoluzione del mercato, le aspettative relative alla qualità da parte degli utenti utenti e la capacità degli sviluppatori di soddisfarle sono rimaste relativamente costanti.

 

Nel pannello inferiore, visualizzato in scala logaritmica:

  • Installazioni medie (linea arancione): mostrano un incremento graduale nel periodo analizzato, con un'accelerazione più marcata negli ultimi anni, raggiungendo oltre un milione di installazioni medie per app nel 2018. Questo trend suggerisce una crescente penetrazione degli smartphone e un aumento dell'engagement degli utenti con le app.

  • Numero di app (barre grigie): evidenzia una crescita esponenziale, arrivando a contare diverse migliaia di app nel 2018. Questa crescita testimonia l'esplosione dell'ecosistema delle app e l'intensificarsi della competizione. È importante notare che la scala logaritmica del grafico attenua visivamente questa crescita che in realtà è molto più accentuata di quanto appaia.

 

Analizzando congiuntamente i due pannelli, emergono relazioni interessanti:

  1. dimensione e complessità crescenti: l'aumento costante della dimensione media delle app è avvenuto in parallelo con la crescita delle installazioni medie e ciò suggerisce che gli utenti non sono stati scoraggiati da app più pesanti, probabilmente perché offrono esperienze più ricche e appaganti e funzionalità maggiori;

  2. stabilità della qualità percepita: nonostante l'aumento della complessità e della dimensione delle app, il rating medio è rimasto stabile.

  3. dinamiche di prezzo: ll sensibile aumento dei prezzi tra 2016 e 2017, seguito da un leggero calo, potrebbe riflettere tentativi di monetizzazione più aggressivi in un mercato maturo, seguiti da aggiustamenti competitivi.

  4. saturazione del mercato: La crescita esponenziale del numero di app, combinata con l'aumento più moderato delle installazioni medie, suggerisce una crescente competizione per l'attenzione degli utenti.

 

Le analisi svolte in questo frammento di codice e le relative visualizzazioni, in conclusione, ci offrono alcune indicazioni preziose per il lancio di una ipotetica nuova app:

  • gli utenti sembrano accettare app di dimensioni maggiori, purché queste offrano valore proporzionato;

  • il mercato è diventato estremamente competitivo, con migliaia di app che si contendono l'attenzione degli utenti;

  • la stabilità delle valutazioni suggerisce che le aspettative di qualità sono ben consolidate;

  • le strategie di prezzo richiedono particolare attenzione, considerando i cambiamenti significativi osservati negli ultimi anni.

In [ ]:
logger = logging.getLogger(__name__)

class CorrelationResults(NamedTuple):
    pearson: pd.DataFrame
    spearman: pd.DataFrame
    figures: List[go.Figure]

class TimeMetrics(NamedTuple):
    data: pd.DataFrame
    figure: go.Figure

@dataclass
class PlayStoreAnalyzer:

    apps_df: pd.DataFrame
    reviews_df: Optional[pd.DataFrame] = None
    max_workers: int = 4

    def __post_init__(self):
        self.apps_df = self.apps_df[self.apps_df['Category'] != '1.9'].copy()
        self.prepare_data()

    def prepare_data(self) -> None:

        # Metriche temporali
        self.apps_df['Last Updated'] = pd.to_datetime(self.apps_df['Last Updated'])

        # Usa la data più recente nel dataset come riferimento
        max_date = self.apps_df['Last Updated'].max()
        self.apps_df['Days_Since_Update'] = (
            max_date - self.apps_df['Last Updated']
        ).dt.days

        self.apps_df['Update_Year'] = self.apps_df['Last Updated'].dt.year

        # Trasformazioni logaritmiche
        self.apps_df['Log_Installs'] = np.log1p(self.apps_df['Installs_Clean'])
        self.apps_df['Log_Size'] = np.log1p(self.apps_df['Size_MB'])

        # Metriche di mercato
        total_installs = self.apps_df['Installs_Clean'].sum()
        self.apps_df['market_share'] = self.apps_df['Installs_Clean'] / total_installs
        self.apps_df['category_share'] = self.apps_df.groupby('Category')['Installs_Clean'].transform(
            lambda x: x / x.sum()
        )

        # Merge con sentiment se disponibile
        if self.reviews_df is not None:
            self._merge_sentiment_data()

    def _merge_sentiment_data(self) -> None:
        sentiment_data = self.reviews_df.merge(
            self.apps_df[['App', 'Category']],
            on='App',
            how='inner'
        )

        app_sentiment = sentiment_data.groupby(['App', 'Category']).agg({
            'Sentiment_Polarity': 'mean',
            'Sentiment_Subjectivity': 'mean'
        }).reset_index()

        self.apps_df = self.apps_df.merge(
            app_sentiment,
            on=['App', 'Category'],
            how='left'
        )

    @staticmethod
    def _create_correlation_heatmap(corr_matrix: pd.DataFrame,
                                  title: str) -> go.Figure:
        return go.Figure(
            data=go.Heatmap(
                z=corr_matrix.values,
                x=corr_matrix.columns,
                y=corr_matrix.index,
                colorscale='RdBu',
                zmin=-1,
                zmax=1,
                text=corr_matrix.values.round(2),
                texttemplate='%{text}',
                textfont={"size": 10}
            ),
            layout=dict(
                title=title,
                height=600,
                font=dict(family="Arial", size=12)
            )
        )

    def _create_scatter_matrix(self, metrics: Dict[str, str]) -> go.Figure:
        scatter_pairs = [
            ('Rating', 'Log_Installs'),
            ('Rating', 'Log_Size'),
            ('Log_Size', 'Log_Installs'),
            ('Rating', 'Days_Since_Update')
        ]

        fig = make_subplots(
            rows=2, cols=2,
            subplot_titles=[
                f'{metrics[x]} vs {metrics[y]}'
                for x, y in scatter_pairs
            ]
        )

        # Creazione di una scala di colori per i giorni
        colorscale = [
            [0, 'green'],      # 0-30 giorni
            [0.1, 'lightgreen'],  # 30-90 giorni
            [0.2, 'yellow'],   # 90-180 giorni
            [0.4, 'orange'],   # 6 mesi-1 anno
            [0.6, 'red'],      # 1-2 anni
            [1.0, 'darkred']   # >2 anni
        ]

        for idx, (x, y) in enumerate(scatter_pairs):
            row = idx // 2 + 1
            col = idx % 2 + 1

            hover_text = [
                f"App: {app}<br>" +
                f"Categoria: {cat}<br>" +
                f"{metrics[x]}: {val_x:.2f}<br>" +
                f"{metrics[y]}: {val_y:.2f}<br>" +
                f"Prezzo: ${price:.2f}<br>" +
                f"Installazioni: {inst:,.0f}<br>" +
                f"Dimensione: {size:.1f}MB<br>" +
                f"Giorni dall'ultimo aggiornamento: {days:.0f}"
                for app, cat, val_x, val_y, price, inst, size, days in zip(
                    self.apps_df['App'],
                    self.apps_df['Category'],
                    self.apps_df[x],
                    self.apps_df[y],
                    self.apps_df['Price_Clean'],
                    self.apps_df['Installs_Clean'],
                    self.apps_df['Size_MB'],
                    self.apps_df['Days_Since_Update']
                )
            ]

            fig.add_trace(
                go.Scatter(
                    x=self.apps_df[x],
                    y=self.apps_df[y],
                    mode='markers',
                    marker=dict(
                        size=4,
                        opacity=0.6,
                        color=self.apps_df['Days_Since_Update'],
                        colorscale=colorscale,
                        colorbar=dict(
                            title='Giorni dall\'ultimo<br>aggiornamento',
                            ticktext=['0', '30', '90', '180', '365', '730', '>730'],
                            tickvals=[0, 30, 90, 180, 365, 730, 1000]
                        ) if idx == 1 else None,
                        cmin=0,
                        cmax=1000
                    ),
                    name=f'{metrics[x]} vs {metrics[y]}',
                    hovertemplate="%{text}<extra></extra>",
                    text=hover_text,
                    showlegend=False
                ),
                row=row, col=col
            )

            # Aggiornamento degli assi
            fig.update_xaxes(title=metrics[x], row=row, col=col, gridcolor='lightgray', showgrid=True)
            fig.update_yaxes(title=metrics[y], row=row, col=col, gridcolor='lightgray', showgrid=True)

        fig.update_layout(
            title='Scatter plots delle relazioni principali',
            height=800,
            width=1000,
            showlegend=False,
            title_x=0.5,
            hovermode='closest',
            plot_bgcolor='white',
            margin=dict(t=100, l=50, r=50, b=50)
        )

        return fig

    def analyze_correlations(self) -> Tuple[pd.DataFrame, pd.DataFrame, List[go.Figure]]:
        metrics = {
            'Rating': 'Rating',
            'Price_Clean': 'Prezzo',
            'Log_Installs': 'Log installazioni',
            'Log_Size': 'Log dimensione',
            'Days_Since_Update': 'Giorni da ultimo aggiornamento'
        }

        # Calcolo correlazioni
        data = self.apps_df[metrics.keys()]
        pearson_corr = data.corr(method='pearson').round(3)
        spearman_corr = data.corr(method='spearman').round(3)

        # Rinomina colonne
        for corr_matrix in [pearson_corr, spearman_corr]:
            corr_matrix.columns = metrics.values()
            corr_matrix.index = metrics.values()

        # Creazione grafici
        figures = []

        # Heatmap correlazioni
        heatmap_fig = make_subplots(
            rows=1, cols=2,
            subplot_titles=('Correlazioni di Pearson', 'Correlazioni di Spearman'),
            horizontal_spacing=0.15
        )

        # Aggiungi heatmap Pearson
        heatmap_fig.add_trace(
            go.Heatmap(
                z=pearson_corr.values,
                x=pearson_corr.columns,
                y=pearson_corr.index,
                colorscale='RdBu',
                zmin=-1,
                zmax=1,
                text=pearson_corr.values.round(2),
                texttemplate='%{text}',
                textfont={"size": 10}
            ),
            row=1, col=1
        )

        # Aggiungi heatmap Spearman
        heatmap_fig.add_trace(
            go.Heatmap(
                z=spearman_corr.values,
                x=spearman_corr.columns,
                y=spearman_corr.index,
                colorscale='RdBu',
                zmin=-1,
                zmax=1,
                text=spearman_corr.values.round(2),
                texttemplate='%{text}',
                textfont={"size": 10}
            ),
            row=1, col=2
        )

        heatmap_fig.update_layout(
            title='Confronto correlazioni Pearson vs Spearman',
            title_x=0.5,
            height=600,
            width=1500,  # Aumentato a 1500
            font=dict(family="Arial", size=12),
            margin=dict(t=100, l=100, r=100, b=50)
        )

        figures.append(heatmap_fig)

        # Scatter matrix
        scatter_pairs = [
            ('Rating', 'Log_Installs'),
            ('Rating', 'Log_Size'),
            ('Log_Size', 'Log_Installs'),
            ('Rating', 'Days_Since_Update')
        ]

        scatter_fig = make_subplots(
            rows=2, cols=2,
            subplot_titles=[
                f'{metrics[x]} vs {metrics[y]}'
                for x, y in scatter_pairs
            ],
            horizontal_spacing=0.15,
            vertical_spacing=0.15
        )

        # Creazione di una scala di colori basata sul rating
        colorscale = [
            [0, 'red'],       # Rating 1
            [0.25, 'orange'], # Rating 2
            [0.5, 'yellow'],  # Rating 3
            [0.75, 'lightgreen'], # Rating 4
            [1, 'green']      # Rating 5
        ]

        for idx, (x, y) in enumerate(scatter_pairs):
            row = idx // 2 + 1
            col = idx % 2 + 1

            hover_text = [
                f"App: {app}<br>" +
                f"Categoria: {cat}<br>" +
                f"{metrics[x]}: {val_x:.2f}<br>" +
                f"{metrics[y]}: {val_y:.2f}<br>" +
                f"Rating: {rating:.1f}<br>" +
                f"Prezzo: ${price:.2f}<br>" +
                f"Installazioni: {inst:,.0f}<br>" +
                f"Dimensione: {size:.1f}MB<br>" +
                f"Giorni dall'ultimo aggiornamento: {days:.0f}"
                for app, cat, val_x, val_y, rating, price, inst, size, days in zip(
                    self.apps_df['App'],
                    self.apps_df['Category'],
                    self.apps_df[x],
                    self.apps_df[y],
                    self.apps_df['Rating'],
                    self.apps_df['Price_Clean'],
                    self.apps_df['Installs_Clean'],
                    self.apps_df['Size_MB'],
                    self.apps_df['Days_Since_Update']
                )
            ]

            if y == 'Days_Since_Update':
                # Aggiunge jitter al rating per evitare sovrapposizioni
                jittered_x = self.apps_df[x] + np.random.normal(0, 0.05, len(self.apps_df))

                # Calcola la media mobile
                rating_range = np.arange(1, 5.1, 0.1)
                days_mean = []
                for r in rating_range:
                    mask = (self.apps_df[x] >= r - 0.2) & (self.apps_df[x] < r + 0.2)
                    mean_val = self.apps_df.loc[mask, y].mean()
                    days_mean.append(mean_val)

                scatter_fig.add_trace(
                    go.Scatter(
                        x=jittered_x,
                        y=self.apps_df[y],
                        mode='markers',
                        marker=dict(
                            size=3,
                            opacity=0.3,
                            color=self.apps_df['Rating'],
                            colorscale=colorscale,
                            colorbar=dict(
                                title='Rating',
                                ticktext=['1', '2', '3', '4', '5'],
                                tickvals=[1, 2, 3, 4, 5]
                            ) if idx == 1 else None,
                            cmin=1,
                            cmax=5
                        ),
                        name=f'{metrics[x]} vs {metrics[y]}',
                        hovertemplate="%{text}<extra></extra>",
                        text=hover_text,
                        showlegend=False
                    ),
                    row=row, col=col
                )

                # Aggiunge la linea della media mobile
                scatter_fig.add_trace(
                    go.Scatter(
                        x=rating_range,
                        y=days_mean,
                        mode='lines',
                        line=dict(color='black', width=2),
                        name='Media mobile',
                        showlegend=False
                    ),
                    row=row, col=col
                )

                # Aggiorna il layout per questo subplot specifico
                scatter_fig.update_xaxes(
                    title=metrics[x],
                    row=row,
                    col=col,
                    gridcolor='lightgray',
                    showgrid=True,
                    range=[0.5, 5.5]
                )

                # Usa il 95° percentile per l'asse y
                y_max = self.apps_df[y].quantile(0.95)
                scatter_fig.update_yaxes(
                    title=metrics[y],
                    row=row,
                    col=col,
                    gridcolor='lightgray',
                    showgrid=True,
                    range=[0, y_max]
                )
            else:
                scatter_fig.add_trace(
                    go.Scatter(
                        x=self.apps_df[x],
                        y=self.apps_df[y],
                        mode='markers',
                        marker=dict(
                            size=4,
                            opacity=0.6,
                            color=self.apps_df['Rating'],
                            colorscale=colorscale,
                            colorbar=dict(
                                title='Rating',
                                ticktext=['1', '2', '3', '4', '5'],
                                tickvals=[1, 2, 3, 4, 5]
                            ) if idx == 1 else None,
                            cmin=1,
                            cmax=5
                        ),
                        name=f'{metrics[x]} vs {metrics[y]}',
                        hovertemplate="%{text}<extra></extra>",
                        text=hover_text,
                        showlegend=False
                    ),
                    row=row, col=col
                )

                scatter_fig.update_xaxes(
                    title=metrics[x],
                    row=row,
                    col=col,
                    gridcolor='lightgray',
                    showgrid=True
                )
                scatter_fig.update_yaxes(
                    title=metrics[y],
                    row=row,
                    col=col,
                    gridcolor='lightgray',
                    showgrid=True
                )

        scatter_fig.update_layout(
            title='Scatter plots delle relazioni principali',
            height=800,
            width=1500,  # Aumentato a 1500
            showlegend=False,
            title_x=0.5,
            hovermode='closest',
            plot_bgcolor='white',
            margin=dict(t=100, l=50, r=50, b=50)
        )

        figures.append(scatter_fig)

        return pearson_corr, spearman_corr, figures


    def analyze_temporal_trends(self) -> TimeMetrics:
        # Verifica preliminare prezzi
        avg_price = self.apps_df.groupby('Update_Year').agg({
            'Price_Clean': lambda x: x[x > 0].mean() if len(x[x > 0]) > 0 else np.nan
        })

        # Aggregazione metriche temporali efficiente
        time_metrics = self.apps_df.groupby('Update_Year').agg({
            'Rating': 'mean',
            'Size_MB': 'mean',
            'Installs_Clean': 'mean',
            'App': 'count'
        }).round(2)

        time_metrics['Price_Clean'] = avg_price['Price_Clean']

        # Creazione figura ottimizzata con subplot
        fig = make_subplots(
            rows=2,
            cols=1,
            row_heights=[0.6, 0.4],
            vertical_spacing=0.12,
            subplot_titles=(
                'Metriche di prodotto (Rating, Prezzo, Dimensione)',
                'Metriche di mercato (Installazioni e numero di app)'
            )
        )

        # Configurazione tracce subplot 1
        traces_subplot1 = [
            ('Rating', 'Rating medio', '#2ECC40'),
            ('Price_Clean', 'Prezzo medio ($)', '#FF4136'),
            ('Size_MB', 'Dimensione media (MB)', '#0074D9')
        ]

        # Aggiunta tracce subplot 1
        for col, name, color in traces_subplot1:
            data = time_metrics[col].fillna(0)
            hover_text = [
                f"Anno: {year}<br>{name}: {self._format_trend_value(val, col.lower())}"
                for year, val in zip(time_metrics.index, time_metrics[col])
            ]

            fig.add_trace(
                go.Scatter(
                    x=time_metrics.index,
                    y=data,
                    name=name,
                    line=dict(color=color, width=2),
                    hovertemplate="%{text}<extra></extra>",
                    text=hover_text
                ),
                row=1,
                col=1
            )

        # Aggiunta barre numero app subplot 2
        hover_text_app = [
            f"Anno: {year}<br>Numero di app: {self._format_trend_value(val, 'app')}"
            for year, val in zip(time_metrics.index, time_metrics['App'])
        ]

        fig.add_trace(
            go.Bar(
                x=time_metrics.index,
                y=time_metrics['App'],
                name='Numero di app',
                marker_color='#AAAAAA',
                opacity=0.3,
                width=0.5,
                hovertemplate="%{text}<extra></extra>",
                text=hover_text_app
            ),
            row=2,
            col=1
        )

        # Aggiunta linea installazioni subplot 2
        hover_text_inst = [
            f"Anno: {year}<br>Installazioni medie: {self._format_trend_value(val, 'installazioni')}"
            for year, val in zip(time_metrics.index, time_metrics['Installs_Clean'])
        ]

        fig.add_trace(
            go.Scatter(
                x=time_metrics.index,
                y=time_metrics['Installs_Clean'],
                name='Installazioni medie',
                line=dict(color='#FF851B', width=2),
                hovertemplate="%{text}<extra></extra>",
                text=hover_text_inst
            ),
            row=2,
            col=1
        )

        # Ottimizzazione layout
        fig.update_layout(
            title={
                'text': 'Evoluzione temporale delle metriche chiave',
                'y': 0.98,
                'x': 0.5,
                'xanchor': 'center',
                'yanchor': 'top'
            },
            height=900,
            showlegend=True,
            legend=dict(
                orientation='h',
                yanchor='bottom',
                y=1.05,
                xanchor='center',
                x=0.5,
                bgcolor='rgba(255, 255, 255, 0.8)',
                bordercolor='lightgray',
                borderwidth=1
            ),
            plot_bgcolor='white',
            hovermode='x unified',
            margin=dict(t=120, b=50, l=50, r=50)
        )

        # Ottimizzazione assi
        for row in [1, 2]:
            fig.update_xaxes(
                title='Anno',
                showgrid=True,
                gridwidth=1,
                gridcolor='lightgray',
                row=row
            )

        fig.update_yaxes(
            title='Valore',
            showgrid=True,
            gridwidth=1,
            gridcolor='lightgray',
            row=1,
            col=1
        )

        fig.update_yaxes(
            title='Numero',
            showgrid=True,
            gridwidth=1,
            gridcolor='lightgray',
            type='log',
            row=2,
            col=1
        )

        return TimeMetrics(time_metrics, fig)

    def _format_trend_value(self, value: float, metric_type: str) -> str:
        if pd.isna(value):
            return "N/D"

        if metric_type == "installazioni":
            if value >= 1e6:
                return f"{value/1e6:.1f}M"
            elif value >= 1e3:
                return f"{value/1e3:.1f}K"
            return f"{value:.0f}"
        elif metric_type == "dimensione":
            return f"{value:.1f}MB"
        elif metric_type == "prezzo":
            return f"${value:.2f}"
        elif metric_type == "rating":
            return f"{value:.2f}"
        elif metric_type == "app":
            return f"{int(value):,}"
        return str(value)

def analyze_play_store(apps_df: pd.DataFrame, reviews_df: Optional[pd.DataFrame] = None) -> Dict[str, Any]:
    logger.info("=== ANALISI GOOGLE PLAY STORE ===")

    # Inizializzazione analyzer
    analyzer = PlayStoreAnalyzer(apps_df, reviews_df)
    figures = []
    results = {}

    try:
        # 1. Analisi correlazioni
        logger.info("\n1. Analisi correlazioni tra metriche")
        pearson_corr, spearman_corr, corr_figures = analyzer.analyze_correlations()
        results['correlations'] = {'pearson': pearson_corr, 'spearman': spearman_corr}
        figures.extend(corr_figures)

        # Log correlazioni
        for method, corr_matrix in [('Pearson', pearson_corr), ('Spearman', spearman_corr)]:
            logger.info(f"\nCorrelazioni {method} significative (|corr| > 0.3):")
            for i in range(len(corr_matrix.columns)):
                for j in range(i+1, len(corr_matrix.columns)):
                    corr = corr_matrix.iloc[i, j]
                    if abs(corr) > 0.3:
                        logger.info(
                            f"{corr_matrix.index[i]} vs {corr_matrix.columns[j]}: {corr:.3f}"
                        )

        # 2. Analisi temporale
        logger.info("\n2. Analisi trend temporali")
        time_metrics, time_fig = analyzer.analyze_temporal_trends()
        results['temporal'] = time_metrics
        figures.append(time_fig)

        # Log trend principali
        first_metrics = time_metrics.iloc[0]
        last_metrics = time_metrics.iloc[-1]

        rating_change = ((last_metrics['Rating'] - first_metrics['Rating']) /
                        first_metrics['Rating'] * 100)

        logger.info("\nTrend principali:")
        logger.info(
            f"Rating medio: {rating_change:+.1f}% di variazione "
            f"(da {first_metrics['Rating']:.2f} a {last_metrics['Rating']:.2f})"
        )
        logger.info(
            f"Dimensione media: da {first_metrics['Size_MB']:.1f}MB a "
            f"{last_metrics['Size_MB']:.1f}MB"
        )

        def format_installs(val):
            return f"{val/1e6:.1f}M" if val >= 1e6 else f"{val/1e3:.1f}K"

        logger.info(
            f"Installazioni medie: da {format_installs(first_metrics['Installs_Clean'])} a "
            f"{format_installs(last_metrics['Installs_Clean'])}"
        )

        logger.info(
            f"Numero di app: da {int(first_metrics['App']):,} a "
            f"{int(last_metrics['App']):,}"
        )

        if pd.notna(first_metrics['Price_Clean']) and pd.notna(last_metrics['Price_Clean']):
            logger.info(
                f"Prezzo medio: da ${first_metrics['Price_Clean']:.2f} a "
                f"${last_metrics['Price_Clean']:.2f}"
            )
        else:
            logger.info("Prezzo medio: dati non disponibili")

        results['figures'] = figures
        return results

    except Exception as e:
        logger.error(f"Errore durante l'analisi del Play Store: {str(e)}")
        raise

# Esecuzione dell'analisi
analysis_results = analyze_play_store(apps_clean, reviews_clean)

# Visualizzazione dei grafici
for fig in analysis_results['figures']:
    fig.show()

Analisi competitiva e tecnica¶

Il codice inizia definendo una classe NamedTuple chiamata MarketStructureResults che funge da contenitore per i risultati dell'analisi della struttura del mercato. Questa classe contiene due elementi:

  • market_df: un dataframme pandas con le metriche calcolate per ogni categoria;

  • figure: un oggetto go.Figure di Plotly per la visualizzazione dei risultati.

Questa struttura serve a separare chiaramente i dati (il dataframe) dalla loro rappresentazione visuale (la figura), permettendo di manipolare entrambi in modo indipendente.

 

La classe MarketAnalyzer si occupa di esaminare la struttura del mercato per ogni categoria di app. Nel costruttore, notiamo la rimozione delle righe dove il valore di Category è '1.9', come visto anche nelle celle precedenti.

Il primo metodo statico, calculate_market_concentration, implementa una versione dell'indice Herfindahl-Hirschman (HHI). Questo indice misura il livello di concentrazione del mercato all'interno di una categoria. L'HHI è calcolato sommando i quadrati delle quote di mercato di ciascuna app (basate sul numero di installazioni) all'interno della categoria. Valori più alti (vicini a 1) indicano un mercato dominato da poche app, mentre valori più bassi indicano un mercato più frammentato con maggiore competizione. La funzione include gestione degli errori tramite blocchi try-except e controlli per casi limite (come categorie vuote), restituendo np.nan quando necessario. La funzione np.clip assicura che il risultato sia compreso tra 0 e 1.

Il metodo calculate_category_stability valuta quanto una categoria è stabile o volatile. Questo indice è costruito considerando tre dimensioni di variabilità:

  1. variabilità del rating: rappresenta la deviazione standard dei rating normalizzata rispetto al valore massimo possibile (5);

  2. variabilità negli aggiornamenti: misura quanto sono diversificati i tempi di aggiornamento delle app nella categoria;

  3. variabilità nelle installazioni: calcola la deviazione standard relativa (coefficiente di variazione) del numero di installazioni dopo trasformazione logaritmica.

L'indice di stabilità finale è una media ponderata inversa di queste variabilità, dove pesi maggiori sono assegnati alla variabilità del rating (40%), seguita dalla variabilità degli aggiornamenti e delle installazioni (30% ciascuna). Un valore più alto (vicino a 1) indica una categoria più stabile, mentre valori più bassi indicano maggiore volatilità.

Il metodo calculate_direct_payment_propensity serve a valutare la propensione al pagamento diretto ed quindi importante per valutare questa tipologia di monetizzazione. Questo indice stima quanto gli utenti in una categoria sono disposti a pagare per le app attraverso tre componenti:

  1. rapporto di app a pagamento: percentuale di app a pagamento nella categoria (40% del peso);

  2. livello dei prezzi: prezzo medio delle app a pagamento normalizzato rispetto al prezzo massimo (30% del peso);

  3. differenza di rating tra app a pagamento e gratuite: trasformata in un valore tra 0 e 1, dove valori più alti indicano che le app a pagamento sono meglio valutate di quelle gratuite (30% del peso).

Un valore più alto dell'indice (vicino a 1) suggerisce una categoria dove gli utenti sono più propensi a pagare per le app, rendendo potenzialmente più fattibili strategie di monetizzazione diretta.

Il metodo analyze_market_structure coordina l'intero processo di analisi, iterando su ogni categoria, calcolando le metriche discusse in precedenza e altre statistiche di base e costruendo un dataframe con i risultati.

Successivamente, crea una visualizzazione a dispersione utilizzando Plotly, dove:

  • l'asse X rappresenta l'indice di concentrazione del mercato;

  • l'asse Y rappresenta l'indice di stabilità;

  • la dimensione dei punti rappresenta la propensione al pagamento diretto;

  • il colore rappresenta il rating medio.

Questa rappresentazione multidimensionale permette di identificare visivamente categorie con caratteristiche desiderabili, come alta stabilità, bassa concentrazione (meno competizione) e alta propensione al pagamento diretto.

 

La classe TechnicalAnalyzer si concentra sugli aspetti tecnici delle app in ciascuna categoria.

Il metodo principale, analyze_development_patterns, esegue un'analisi completa degli aspetti tecnici per ogni categoria. Esso analizza:

  • la distribuzione delle versioni Android supportate dalle app in ogni categoria;

  • statistiche sulla dimensione delle app (media, mediana, deviazione standard);

  • la frequenza di aggiornamento (aggiornamenti per anno).

Il codice crea due visualizzazioni:

  1. _create_android_distribution_plot: un grafico a barre orizzontali impilate che mostra la distribuzione percentuale delle versioni Android per categoria;

  2. _create_technical_details_plot: un box plot che illustra la distribuzione delle dimensioni delle app per categoria, evidenziando gli outlier.

Nello specifico, il metodo _create_android_distribution_plot ordina le categorie calcolando una media ponderata e moltiplicando ogni versione di Android per la sua percentuale di distribuzione, permettendo quindi di ordinare le categorie da quelle che supportano versioni più recenti a quelle che supportano versioni più datate.

Il metodo _create_technical_details_plot visualizza la distribuzione delle dimensioni delle app per categoria, mostrando mediana, quartili e outlier. L'utilizzo di customdata per inserire i nomi delle app nei punti outlier è un tocco di design interessante che permette all'utente di identificare direttamente quali app sono significativamente più grandi della media nella loro categoria.

La funzione perform_category_analysis gestisce l'intero processo di analisi competitiva, occupandosi infatti di esegurire entrambe le analisi (di mercato e tecnica) e poi calcolando un punteggio complessivo per ogni categoria. Il punteggio finale è una combinazione ponderata di:

  1. market score (80% del punteggio finale), che include:
  • stabilità della categoria (40% del market score);

  • inverso della concentrazione del mercato (30% del market score) - più bassa è la concentrazione, più alto è questo componente;

  • propensione al pagamento (30% del market score).

  1. technical score (20% del punteggio finale), basato principalmente sulla frequenza di aggiornamento normalizzata.

Il calcolo di questi punteggi rappresenta di fatto un modello di scoring che privilegia categorie con mercati stabili, competizione non dominata da pochi attori e con utenti disposti a pagare per gli applicativi. L'aspetto tecnico ha un peso minore, ma favorisce categorie con aggiornamenti più frequenti, indicativi di un ecosistema più attivo e dinamico.

Infine, la funzione identifica le top 5 categorie più promettenti in base al punteggio finale e genera un breve report.

 

La parte finale del blocco di codice esegue la funzione perform_category_analysis passando come parametri i dataframe apps_clean e reviews_clean, che sono stati preprocessati nei blocchi precedenti. La funzione restituisce due oggetti: un dizionario results contenente i risultati numerici dell'analisi e una lista figures con le visualizzazioni generate.

Questa implementazione itera attraverso la lista di figure con un ciclo for e le visualizza sequenzialmente utilizzando il metodo show() di Plotly. Da notare che i risultati numerici contenuti nel dizionario results non vengono esplicitamente visualizzati nel notebook, ma solo registrati nei log attraverso le chiamate a logger.info() all'interno della funzione.

 

Interpretazione dei risultati¶

 

Struttura del mercato per categoria¶

Questo scatter plot mappa le categorie di app in base a due dimensioni cruciali: l'indice di concentrazione del mercato (asse X) e l'indice di stabilità (asse Y). La grandezza dei cerchi rappresenta la propensione al pagamento diretto, mentre il colore indica la valutazione media, con una gradazione che va dal rosso per quelle più basse al verde per quelle più alte.

Nella parte superiore sinistra troviamo ENTERTAINMENT, caratterizzata da un'alta stabilità (0.77) e una bassa concentrazione (circa 0.05). Questa posizione descrive un mercato equilibrato, dove nessuna app domina eccessivamente e le condizioni rimangono relativamente costanti nel tempo. La dimensione piuttosto grande del cerchio suggerisce inoltre una buona propensione degli utenti a pagare per contenuti di intrattenimento.

ART_AND_DESIGN si distingue per la sua posizione nella parte superiore destra del grafico, combinando alta stabilità (circa 0.78) con una concentrazione moderata (circa 0.25). Il colore verde intenso indica un rating medio molto alto. Questa configurazione rappresenta un mercato stabile dove, nonostante la presenza di alcuni player dominanti, la qualità sembra venga premiata e potrebbero esserci opportunità per nuovi entranti.

Nella parte centrale del grafico troviamo categorie come MEDICAL e PERSONALIZATION, rappresentate da cerchi di grandi dimensioni che indicano un'elevata propensione degli utenti a pagare per queste tipologie di app. Tuttavia, la loro posizione relativamente bassa sull'asse della stabilità (circa 0.45-0.50) suggerisce mercati più volatili dove le condizioni possono cambiare rapidamente.

WEATHER, HEALTH_AND_FITNESS e NEWS_AND_MAGAZINES mostrano una concentrazione relativamente alta, ma dimensioni dei cerchi variabili, rappresentando nicchie di mercato dove pochi player dominanti controllano gran parte delle installazioni. Queste categorie potrebbero richiedere strategie di differenziazione più marcate per nuovi entranti.

Nella parte inferiore del grafico si collocano categorie come FAMILY, GAME e TOOLS, caratterizzate da bassa stabilità e bassa concentrazione. Questi sono mercati altamente frammentati e volatili con numerosi competitor, dove il successo potrebbe essere più difficile da mantenere nel lungo periodo.

 

Distribuzione versioni Android per categoria¶

In questo grafico viene presentata un'analisi tecnica attraverso barre orizzontali impilate che mostrano la distribuzione percentuale delle versioni Android supportate dalle app in ciascuna categoria. Le versioni più recenti sono rappresentate in verde, mentre quelle più datate in rosso, creando un gradiente cromatico che facilita l'identificazione dei trend tecnologici.

L'adozione delle versioni più recenti di Android varia significativamente tra le categorie. ENTERTAINMENT, FOOD_AND_DRINK e TRAVEL_AND_LOCAL si distinguono per la maggiore percentuale di app che supportano le versioni più recenti. Ciò potrebbe riflettere la necessità di sfruttare le funzionalità avanzate delle nuove versioni del sistema operativo.

All'estremo opposto, categorie come LIBRARIES_AND_DEMO, BOOKS_AND_REFERENCE e COMMUNICATION mostrano percentuali significative di app che supportano ancora versioni più datate di Android (2.0-3.0). Questo approccio più conservativo potrebbe derivare dalla necessità di mantenere la compatibilità con un parco dispositivi più ampio, o dalla presenza di app storiche che non vengono aggiornate frequentemente.

La categoria GAME mostra una distribuzione relativamente uniforme attraverso le diverse versioni di Android, riflettendo probabilmente la necessità di raggiungere il pubblico più ampio possibile, dai dispositivi più datati a quelli più recenti, data l'importanza del volume di utenti in questo segmento altamente competitivo.

Questa analisi fornisce alcune indicazioni per gli sviluppatori su quanto sia importante il supporto multi-versione nelle diverse categorie. Per alcune, l'adozione rapida delle ultime tecnologie potrebbe rappresentare un vantaggio competitivo, mentre per altre un approccio più inclusivo potrebbe massimizzare la base di utenti potenziali.

 

Distribuzione dimensioni app per categoria¶

Questo grafico utilizza box plot per visualizzare la distribuzione delle dimensioni delle app (in MB) per ciascuna categoria. Ogni box plot mostra la mediana (linea centrale), il range interquartile (il "box" che rappresenta il 50% centrale dei dati) e gli outlier (punti individuali che si discostano significativamente dalla distribuzione principale).

GAME, FAMILY e MEDICAL emergono come le categorie con le dimensioni mediane più elevate, oscillando tra 20 e 40 MB. I box più ampi in queste categorie indicano anche una maggiore variabilità nelle dimensioni.

Categorie come LIBRARIES_AND_DEMO, PRODUCTIVITY e COMMUNICATION tendono invece a presentare dimensioni mediane più contenute, generalmente tra 5 e 15 MB. Questa leggerezza suggerisce un focus maggiore sull'efficienza e la funzionalità piuttosto che su contenuti multimediali elaborati.

Un aspetto ricorrente in quasi tutte le categorie è la presenza di outlier significativi, visualizzati come punti al di sopra dei box. Questi rappresentano app che si discostano notevolmente dalla tendenza centrale della loro categoria, raggiungendo in alcuni casi dimensioni di 80-100 MB. Particolarmente degni di nota sono gli outlier nelle categorie GAME, FAMILY e MEDICAL.

La variabilità all'interno delle categorie fornisce ulteriori spunti interpretativi. GAME mostra il range interquartile più ampio, indicando la maggiore eterogeneità nelle dimensioni. Questa caratteristica riflette probabilmente la diversità dei generi di gioco disponibili, dai semplici puzzle che richiedono poche risorse ai giochi 3D complessi con asset grafici elaborati. Al contrario, categorie come WEATHER, PRODUCTIVITY e LIBRARIES_AND_DEMO presentano box più compatti, suggerendo maggiore omogeneità nelle dimensioni e, potenzialmente, una maggiore standardizzazione nelle pratiche di sviluppo.

 

In sintesi, dalle tre visualizzazioni presenti in questa parte di codice emergono alcune considerazioni strategiche rilevanti per chi intende sviluppare una nuova app.

Il modello di scoring utilizzato identifica ENTERTAINMENT come categoria più promettente (0.785), seguita da FOOD_AND_DRINK (0.764), ART_AND_DESIGN (0.740), DATING (0.735) e SHOPPING (0.733).

Category final_score market_score technical_score
ENTERTAINMENT 0.785 0.733 0.993
FOOD_AND_DRINK 0.764 0.709 0.984
ART_AND_DESIGN 0.740 0.680 0.978
DATING 0.735 0.669 1.000
SHOPPING 0.733 0.672 0.976

Questi risultati derivano da una formula che assegna l'80% del peso al market score (combinazione di stabilità, bassa concentrazione e propensione al pagamento) e il 20% al technical score (basato principalmente sulla frequenza di aggiornamento). È importante notare che questa metodologia può generare classifiche che sembrano contrastare con quanto osservabile nei grafici. Ad esempio, ART_AND_DESIGN appare particolarmente favorevole nel grafico della struttura di mercato (alta stabilità e rating eccellente), ma è solo terza nella classifica finale.

Ciò spiega perché categorie come DATING, con technical score perfetto (1.000), risultino alte in classifica nonostante caratteristiche di mercato non ottimali. Questa discrepanza evidenzia l'importanza di considerare sia i risultati numerici che le visualizzazioni grafiche per una comprensione completa delle opportunità di mercato.

Le visualizzazioni offrono quindi una base empirica per navigare l'ecosistema del Google Play Store, ma è consigliabile valutare criticamente i pesi assegnati ai diversi fattori in base alle proprie priorità strategiche.

In [ ]:
logger = logging.getLogger(__name__)

class MarketStructureResults(NamedTuple):
    market_df: pd.DataFrame
    figure: go.Figure

class MarketAnalyzer:

    def __init__(self, apps_df: pd.DataFrame, max_workers: int = 4):
        self.apps_df = apps_df[apps_df['Category'] != '1.9'].copy()
        self.max_workers = max_workers

    @staticmethod
    def calculate_market_concentration(category_data: pd.DataFrame) -> float:
        try:
            if len(category_data) == 0:
                return np.nan

            total_installs = category_data['Installs_Clean'].sum()
            if total_installs == 0:
                return np.nan

            market_shares = category_data['Installs_Clean'] / total_installs
            hhi = (market_shares ** 2).sum()

            return np.clip(hhi, 0, 1)
        except:
            return np.nan

    @staticmethod
    def calculate_category_stability(category_data: pd.DataFrame) -> float:
        try:
            if len(category_data) < 2:
                return np.nan

            # Calcolo variabilità rating
            rating_var = (category_data['Rating'].std() / 5
                         if not category_data['Rating'].isna().all() else 0)

            # Calcolo variabilità aggiornamenti
            update_var = (category_data['Days_Since_Update'].std() / 365
                         if not category_data['Days_Since_Update'].isna().all() else 0)

            # Calcolo variabilità installazioni
            installs = np.log1p(category_data['Installs_Clean'])
            install_var = (installs.std() / installs.mean()
                          if not installs.isna().all() and installs.mean() > 0 else 0)

            # Calcolo stabilità complessiva
            stability = 1 - (rating_var * 0.4 + update_var * 0.3 + install_var * 0.3)
            return np.clip(stability, 0, 1)
        except:
            return np.nan

    @staticmethod
    def calculate_direct_payment_propensity(category_data: pd.DataFrame) -> float:
        try:
            if len(category_data) == 0:
                return np.nan

            # Calcolo ratio app a pagamento
            paid_apps = category_data[category_data['Price_Clean'] > 0]
            paid_ratio = len(paid_apps) / len(category_data)

            # Calcolo livello prezzi
            max_price = category_data['Price_Clean'].max()
            price_level = (paid_apps['Price_Clean'].mean() / max_price
                         if len(paid_apps) > 0 and max_price > 0 else 0)

            # Calcolo differenza rating paid vs free
            if len(paid_apps) > 0:
                free_rating = category_data[category_data['Price_Clean'] == 0]['Rating'].mean()
                paid_rating = paid_apps['Rating'].mean()
                paid_vs_free = (paid_rating - free_rating + 5) / 10
            else:
                paid_vs_free = 0

            # Calcolo propensione complessiva
            propensity = (paid_ratio * 0.4 + price_level * 0.3 + paid_vs_free * 0.3)
            return np.clip(propensity, 0, 1)
        except:
            return np.nan

    def analyze_market_structure(self) -> MarketStructureResults:

        # Lista per raccogliere risultati
        results = []

        # Analisi per categoria
        for category in self.apps_df['Category'].unique():
            category_data = self.apps_df[self.apps_df['Category'] == category]

            results.append({
                'Category': category,
                'num_apps': len(category_data),
                'concentration': self.calculate_market_concentration(category_data),
                'stability': self.calculate_category_stability(category_data),
                'payment_propensity': self.calculate_direct_payment_propensity(category_data),
                'avg_rating': category_data['Rating'].mean(),
                'total_installs': category_data['Installs_Clean'].sum()
            })

        # Creazione DataFrame risultati
        market_df = pd.DataFrame(results).set_index('Category')

        # Creazione grafico
        fig = go.Figure(data=[
            go.Scatter(
                x=market_df['concentration'],
                y=market_df['stability'],
                mode='markers+text',
                text=market_df.index,
                textposition='top right',
                textfont=dict(size=9),
                marker=dict(
                    size=market_df['payment_propensity'].fillna(0) * 50 + 20,
                    color=market_df['avg_rating'].fillna(market_df['avg_rating'].mean()),
                    colorscale='RdYlGn',
                    colorbar=dict(title='Rating medio'),
                    showscale=True
                ),
                hovertemplate=(
                    "<b>%{text}</b><br>" +
                    "Concentrazione: %{x:.3f}<br>" +
                    "Stabilità: %{y:.3f}<br>" +
                    "Propensione pagamento: %{marker.size:.3f}<br>" +
                    "Rating: %{marker.color:.2f}<br>" +
                    "<extra></extra>"
                )
            )
        ])

        # Aggiornamento layout
        fig.update_layout(
            title=dict(
                text='Struttura del mercato per categoria',
                x=0.5,
                y=0.95,
                xanchor='center',
                yanchor='top',
                font=dict(size=20)
            ),
            xaxis_title='Indice di concentrazione del mercato',
            yaxis_title='Indice di stabilità',
            height=800,
            showlegend=False,
            plot_bgcolor='white',
            margin=dict(t=150),
            annotations=[
                dict(
                    text='Dimensione = propensione al pagamento diretto',
                    xref='paper',
                    yref='paper',
                    x=0.5,
                    y=1.08,
                    xanchor='center',
                    yanchor='middle',
                    showarrow=False,
                    font=dict(size=11)
                ),
                dict(
                    text='Colore = rating medio',
                    xref='paper',
                    yref='paper',
                    x=0.5,
                    y=1.04,
                    xanchor='center',
                    yanchor='middle',
                    showarrow=False,
                    font=dict(size=11)
                )
            ]
        )

        return MarketStructureResults(market_df, fig)


class TechnicalAnalyzer:

    def __init__(self, apps_df: pd.DataFrame, max_workers: int = 4):
        self.apps_df = apps_df[apps_df['Category'] != '1.9'].copy()
        self.max_workers = max_workers

    def analyze_development_patterns(self) -> Tuple[pd.DataFrame, List[go.Figure]]:
        """Analizza i pattern di sviluppo per ogni categoria"""
        tech_results = []
        android_distributions = {}

        # Calcolo distribuzioni versioni Android per categoria
        categories = self.apps_df['Category'].unique()
        for category in categories:
            category_data = self.apps_df[self.apps_df['Category'] == category]

            # Distribuzione versioni Android
            version_dist = category_data['Android_Ver_Clean'].value_counts()
            total_apps = len(category_data)
            version_percentages = (version_dist / total_apps * 100).round(1)
            android_distributions[category] = version_percentages

            # Statistiche tecniche
            size_stats = {
                'mean': category_data['Size_MB'].mean(),
                'median': category_data['Size_MB'].median(),
                'std': category_data['Size_MB'].std()
            }

            days_since = category_data['Days_Since_Update'].mean()
            updates_per_year = 365 / days_since if days_since > 0 else np.nan

            tech_results.append({
                'Category': category,
                'avg_size': size_stats['mean'],
                'size_variability': size_stats['std'] / size_stats['mean'] if size_stats['mean'] > 0 else 0,
                'updates_per_year': updates_per_year,
                'num_apps': total_apps
            })

        # Creazione matrice versioni Android
        all_versions = sorted(set().union(*[dist.index for dist in android_distributions.values()]))
        android_matrix = pd.DataFrame(
            index=android_distributions.keys(),
            columns=all_versions,
            data=0.0
        )

        # Popolamento matrice
        for category, dist in android_distributions.items():
            for version in dist.index:
                android_matrix.loc[category, version] = dist[version]

        tech_df = pd.DataFrame(tech_results).set_index('Category')

        # Creazione grafici
        android_fig = self._create_android_distribution_plot(android_matrix)
        tech_fig = self._create_technical_details_plot(tech_df)

        return tech_df, [android_fig, tech_fig]

    def _create_android_distribution_plot(self, android_matrix: pd.DataFrame) -> go.Figure:

        # Ordinamento categorie per versione media
        weighted_avg = pd.DataFrame({
            'avg_version': sum(android_matrix[col] * float(col) for col in android_matrix.columns) / 100
        })
        android_matrix_sorted = android_matrix.loc[weighted_avg.sort_values('avg_version', ascending=False).index]

        # Scala colori per versioni
        n_versions = len(android_matrix_sorted.columns)
        colors = [
            f'rgb({int(255*(1-i/n_versions))}, {int(255*(i/n_versions))}, 0)'
            for i in range(n_versions)
        ]

        # Creazione grafico
        fig = go.Figure()

        for i, version in enumerate(android_matrix_sorted.columns):
            fig.add_trace(go.Bar(
                name=f'Android {version}',
                y=android_matrix_sorted.index,
                x=android_matrix_sorted[version],
                orientation='h',
                marker_color=colors[i],
                hovertemplate=(
                    "<b>%{y}</b><br>" +
                    f"Android {version}: " + "%{x:.1f}%<br>" +
                    "<extra></extra>"
                )
            ))

        fig.update_layout(
            title=dict(
                text='Distribuzione versioni Android per categoria',
                x=0.5,
                font=dict(size=20)
            ),
            xaxis_title='Percentuale di app (%)',
            yaxis_title='Categoria',
            barmode='stack',
            height=800,
            showlegend=True,
            plot_bgcolor='white',
            legend=dict(
                title='Versione Android',
                yanchor="top",
                y=0.99,
                xanchor="left",
                x=1.02
            ),
            margin=dict(l=200, r=150),
            bargap=0.1
        )

        fig.add_vline(x=100, line_dash="dash", line_color="gray", opacity=0.5)

        return fig

    def _create_technical_details_plot(self, tech_df: pd.DataFrame) -> go.Figure:
        categories = tech_df.index.tolist()
        fig = go.Figure()

        for category in categories:
            category_data = self.apps_df[self.apps_df['Category'] == category]
            category_sizes = category_data['Size_MB'].dropna()
            app_names = category_data.loc[category_sizes.index, 'App'].values

            fig.add_trace(go.Box(
                y=category_sizes,
                name=category,
                boxpoints='outliers',
                jitter=0.3,
                pointpos=-1.8,
                hovertemplate=(
                    "<b>%{customdata}</b><br>" +
                    "Categoria: %{x}<br>" +
                    "Dimensione: %{y:.1f}MB<br>" +
                    "<extra></extra>"
                ),
                customdata=app_names
            ))

        fig.update_layout(
            title=dict(
                text='Distribuzione dimensioni app per categoria',
                x=0.5,
                font=dict(size=20)
            ),
            xaxis=dict(
                title='Categoria',
                tickangle=45,
                tickfont=dict(size=10)
            ),
            yaxis_title='Dimensione (MB)',
            showlegend=False,
            height=700,
            margin=dict(b=150, t=150),
            plot_bgcolor='white',
            annotations=[dict(
                text=('Box = range interquartile (25°-75° percentile)<br>' +
                      'Linea = mediana<br>' +
                      'Punti = outlier (app significativamente più pesanti)'),
                xref='paper',
                yref='paper',
                x=0.5,
                y=1.1,
                showarrow=False,
                font=dict(size=12)
            )]
        )

        return fig

def perform_category_analysis(apps_df: pd.DataFrame, reviews_df: pd.DataFrame) -> Dict[str, Any]:
    logger.info("=== ANALISI APPROFONDITA PER CATEGORIA ===\n")
    results = {}
    figures = []

    # 1. Analisi struttura mercato
    logger.info("\n1. Analisi struttura del mercato")
    market_analyzer = MarketAnalyzer(apps_df)
    market_results = market_analyzer.analyze_market_structure()
    results['market'] = market_results.market_df
    figures.append(market_results.figure)

    # Log risultati mercato
    logger.info("\nTop 5 categorie per concentrazione di mercato:")
    print(market_results.market_df[['concentration', 'stability', 'avg_rating']]
          .nlargest(5, 'concentration'))

    # 2. Analisi tecnica
    logger.info("\n2. Analisi aspetti tecnici")
    tech_analyzer = TechnicalAnalyzer(apps_df)
    tech_df, tech_figs = tech_analyzer.analyze_development_patterns()
    results['technical'] = tech_df
    figures.extend(tech_figs)

    # Calcolo score finale
    categories = market_results.market_df.index
    final_scores = pd.DataFrame(index=categories)

    # Market score (80%)
    final_scores['market_score'] = (
        market_results.market_df['stability'] * 0.4 +
        (1 - market_results.market_df['concentration']) * 0.3 +
        market_results.market_df['payment_propensity'].fillna(0) * 0.3
    )

    # Technical score (20%)
    final_scores['technical_score'] = (
        tech_df['updates_per_year'].fillna(0) / tech_df['updates_per_year'].max()
    )

    # Score finale
    final_scores['final_score'] = (
        final_scores['market_score'] * 0.8 +
        final_scores['technical_score'] * 0.2
    )

    results['final_scores'] = final_scores

    # Report finale
    logger.info("\nTop 5 categorie più promettenti:")
    top_5 = final_scores.nlargest(5, 'final_score')
    for cat in top_5.index:
        logger.info(f"\n{cat}:")
        logger.info(f"- Score finale: {top_5.loc[cat, 'final_score']:.3f}")
        logger.info(f"- Score mercato: {top_5.loc[cat, 'market_score']:.3f}")
        logger.info(f"- Score tecnico: {top_5.loc[cat, 'technical_score']:.3f}")
        logger.info(f"- Dettagli mercato:")
        logger.info(f"  * Concentrazione: {market_results.market_df.loc[cat, 'concentration']:.3f}")
        logger.info(f"  * Stabilità: {market_results.market_df.loc[cat, 'stability']:.3f}")
        logger.info(f"  * Propensione al pagamento: {market_results.market_df.loc[cat, 'payment_propensity']:.3f}")

    # Visualizzazione esplicita delle top 5 categorie
    print("\n======== TOP 5 CATEGORIE PIÙ PROMETTENTI ========")
    print(top_5[['final_score', 'market_score', 'technical_score']].round(3))

    print("\nNOTA SULLA METODOLOGIA:")
    print("Il ranking è basato su una formula che assegna:")
    print("- 80% al market score: stabilità (40%), bassa concentrazione (30%), propensione al pagamento (30%)")
    print("- 20% al technical score: principalmente frequenza di aggiornamento normalizzata")
    print("\nQuesta metodologia potrebbe generare risultati che sembrano in contrasto con alcune")
    print("visualizzazioni grafiche, dove categorie come ART_AND_DESIGN appaiono più favorevoli.")
    print("Il peso significativo dato alla frequenza di aggiornamento spiega perché categorie")
    print("come DATING, con technical score perfetto (1.000), risultino alte in classifica.")

    return results, figures

# Esecuzione dell'analisi
results, figures = perform_category_analysis(apps_clean, reviews_clean)

# Visualizzazione dei grafici
for fig in figures:
    fig.show()
                    concentration  stability  avg_rating
Category                                                
HEALTH_AND_FITNESS          0.313      0.554       4.226
ART_AND_DESIGN              0.265      0.789       4.357
NEWS_AND_MAGAZINES          0.222      0.556       4.143
PARENTING                   0.186      0.749       4.300
WEATHER                     0.180      0.493       4.231

======== TOP 5 CATEGORIE PIÙ PROMETTENTI ========
                final_score  market_score  technical_score
Category                                                  
ENTERTAINMENT         0.785         0.733            0.993
FOOD_AND_DRINK        0.764         0.709            0.984
ART_AND_DESIGN        0.740         0.680            0.978
DATING                0.735         0.669            1.000
SHOPPING              0.733         0.672            0.976

NOTA SULLA METODOLOGIA:
Il ranking è basato su una formula che assegna:
- 80% al market score: stabilità (40%), bassa concentrazione (30%), propensione al pagamento (30%)
- 20% al technical score: principalmente frequenza di aggiornamento normalizzata

Questa metodologia potrebbe generare risultati che sembrano in contrasto con alcune
visualizzazioni grafiche, dove categorie come ART_AND_DESIGN appaiono più favorevoli.
Il peso significativo dato alla frequenza di aggiornamento spiega perché categorie
come DATING, con technical score perfetto (1.000), risultino alte in classifica.

Conclusione¶

L'analisi del mercato delle app sul Google Play Store ha rivelato un ecosistema complesso e in continua evoluzione, offrendo inoltre preziose indicazioni per gli stakeholder interessati a investire in questo settore.

Il panorama è caratterizzato da una marcata disomogeneità nella distribuzione delle applicazioni, con categorie come FAMILY e GAME che dominano in termini numerici, rappresentando insieme circa un terzo del mercato complessivo. Tuttavia, questa concentrazione non si traduce necessariamente in opportunità limitate per i nuovi entranti. Al contrario, l'analisi ha evidenziato come le categorie meno affollate tendano a registrare valutazioni medie più elevate, suggerendo che in mercati di nicchia è possibile emergere con prodotti di qualità capaci di soddisfare le aspettative degli utenti.

Le strategie di monetizzazione mostrano pattern degni di attenzione: la maggioranza delle app a pagamento (36.4%) si posiziona nella fascia di prezzo 1-2.99$. È interessante notare come le applicazioni con prezzi più contenuti tendano a ricevere valutazioni superiori, mentre quelle premium spesso faticano a soddisfare le aspettative elevate generate dal loro costo.

L'analisi competitiva ha portato all'identificazione di alcune opportunità potenzialmente promettenti. Il modello di scoring, che integra metriche di mercato e tecniche, ha identificato ENTERTAINMENT, FOOD_AND_DRINK e ART_AND_DESIGN come le categorie più favorevoli. Queste categorie combinano una buona stabilità del mercato con una concentrazione relativamente bassa e, nel caso di ART_AND_DESIGN, valutazioni molto positive.

Le correlazioni tra le diverse metriche rivelano alcuni insight strategici: la frequenza di aggiornamento emerge come un fattore cruciale correlato al successo delle app, con una chiara relazione tra aggiornamenti recenti, valutazioni migliori e maggior numero di installazioni. Anche la dimensione dell'applicazione mostra una correlazione positiva con le installazioni, suggerendo che gli utenti apprezzano app più ricche di funzionalità nonostante il maggior spazio richiesto.

Un'analisi più granulare rivela che le categorie differiscono significativamente nelle loro caratteristiche tecniche. Le app di intrattenimento, cibo e viaggi tendono ad adottare più rapidamente le versioni recenti di Android, mentre categorie come libri e comunicazione mantengono la compatibilità con versioni più datate.

Queste evidenze suggeriscono alcune considerazioni strategiche. Le nicchie meno affollate offrono opportunità per emergere con prodotti di qualità, mentre i settori più competitivi richiedono strategie di differenziazione più marcate. Gli aggiornamenti frequenti e la manutenzione costante dell'app sembrano essere comunque essenziali per mantenere alti livelli di gradimento e crescere nelle installazioni. La propensione al pagamento varia significativamente tra le categorie, suggerendo strategie di monetizzazione diverse a seconda del settore scelto.

 

Raccomandazioni strategiche¶

MEDICAL emerge come categoria particolarmente favorevole quando si considerano tutte le dimensioni analizzate. Con un buon rating medio di 4.2, si posiziona nella fascia alta della soddisfazione utenti, pur mantenendo una competizione moderata con 439 app. La sua dimensione media relativamente elevata (20-40 MB) indica applicazioni funzionalmente ricche, mentre l'altissima propensione al pagamento (22.6% di app a pagamento) suggerisce utenti disposti a investire per soluzioni di qualità. Sebbene presenti una stabilità di mercato media, questo dinamismo può rappresentare un'opportunità per nuovi entranti con idee sufficientemente innovative. L'analisi delle correlazioni suggerisce inoltre che in questa categoria gli aggiornamenti regolari e la dimensione adeguata dell'app sono particolarmente premianti in termini di installazioni.

ART_AND_DESIGN offre un equilibrio ottimale tra opportunità di mercato e sostenibilità a lungo termine. La categoria si distingue per l'eccellente rating medio (4.3) e l'elevata stabilità di mercato (0.78), indicando utenti soddisfatti e condizioni competitive relativamente costanti nel tempo. La concentrazione moderata (0.25) suggerisce che, nonostante la presenza di alcuni player dominanti, c'è spazio per nuovi entranti con prodotti di qualità. L'evoluzione temporale mostra inoltre un trend di crescita costante sia nelle installazioni che nelle dimensioni medie delle app, segnalando un mercato in espansione. La distribuzione delle versioni Android indica una buona adozione di versioni recenti, permettendo l'implementazione di nuove features senza sacrificare la base utenti.

ENTERTAINMENT completa il trittico di opportunità raccomandate, distinguendosi per la combinazione unica di alta stabilità (0.77) e bassissima concentrazione (0.05). Questa configurazione descrive un mercato equilibrato dove nessuna app domina eccessivamente, creando un ambiente favorevole per i nuovi entranti. La categoria ha mostrato una crescita costante delle installazioni medie negli ultimi anni, con una buona propensione degli utenti a pagare per contenuti di qualità. L'analisi della distribuzione delle versioni Android rivela inoltre una delle percentuali più alte di adozione delle versioni recenti, indicando un pubblico tecnologicamente aggiornato e potenzialmente più ricettivo a soluzioni più all'avanguardia. Le correlazioni osservate confermano che in questa categoria la frequenza di aggiornamento è particolarmente premiante, con una forte relazione tra aggiornamenti recenti e rating elevati.

 

Dashboard Streamlit¶

Per facilitare l'esplorazione di questi dati ho realizzato una dashboard interattiva in Streamlit. Questo strumento permette di visualizzare dinamicamente le metriche analizzate, filtrare i dati per categoria e generare insights personalizzati in base a parametri configurabili.